diff --git a/CODEOWNERS b/CODEOWNERS index 16a0fb25e1b2f..9b767694b01ba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,6 +9,7 @@ /bundles/org.openhab.automation.jsscripting/ @jpg0 /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers /bundles/org.openhab.automation.pidcontroller/ @fwolter +/bundles/org.openhab.automation.pwm/ @fwolter /bundles/org.openhab.binding.adorne/ @theiding /bundles/org.openhab.binding.ahawastecollection/ @soenkekueper /bundles/org.openhab.binding.airq/ @aurelio1 @@ -25,6 +26,7 @@ /bundles/org.openhab.binding.autelis/ @digitaldan /bundles/org.openhab.binding.automower/ @maxpg /bundles/org.openhab.binding.avmfritz/ @cweitkamp +/bundles/org.openhab.binding.benqprojector/ @mlobstein /bundles/org.openhab.binding.bigassfan/ @mhilbush /bundles/org.openhab.binding.bluetooth/ @cdjackson @cpmeister /bundles/org.openhab.binding.bluetooth.airthings/ @paulianttila @@ -165,6 +167,7 @@ /bundles/org.openhab.binding.meteoblue/ @9037568 /bundles/org.openhab.binding.meteostick/ @cdjackson /bundles/org.openhab.binding.miele/ @kgoderis +/bundles/org.openhab.binding.mielecloud/ @BjoernLange /bundles/org.openhab.binding.mihome/ @pboos /bundles/org.openhab.binding.miio/ @marcelrv /bundles/org.openhab.binding.milight/ @davidgraeff @@ -229,6 +232,7 @@ /bundles/org.openhab.binding.playstation/ @FluBBaOfWard /bundles/org.openhab.binding.plclogo/ @falkena /bundles/org.openhab.binding.plugwise/ @wborn +/bundles/org.openhab.binding.plugwiseha/ @lsiepel /bundles/org.openhab.binding.powermax/ @lolodomo /bundles/org.openhab.binding.pulseaudio/ @peuter /bundles/org.openhab.binding.pushbullet/ @hakan42 @@ -248,7 +252,8 @@ /bundles/org.openhab.binding.sagercaster/ @clinique /bundles/org.openhab.binding.samsungtv/ @paulianttila /bundles/org.openhab.binding.satel/ @druciak -/bundles/org.openhab.binding.senechome/ @vctender @KorbinianP +/bundles/org.openhab.binding.semsportal/ @itb3 +/bundles/org.openhab.binding.senechome/ @vctender @KorbinianP @eguib /bundles/org.openhab.binding.seneye/ @nikotanghe /bundles/org.openhab.binding.sensebox/ @hakan42 /bundles/org.openhab.binding.sensibo/ @seime diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 57f14d56c82d7..b46effe12bb9f 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -36,6 +36,11 @@ org.openhab.automation.pidcontroller ${project.version} + + org.openhab.addons.bundles + org.openhab.automation.pwm + ${project.version} + org.openhab.addons.bundles org.openhab.binding.adorne @@ -116,6 +121,11 @@ org.openhab.binding.avmfritz ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.benqprojector + ${project.version} + org.openhab.addons.bundles org.openhab.binding.bigassfan @@ -811,6 +821,11 @@ org.openhab.binding.miele ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.mielecloud + ${project.version} + org.openhab.addons.bundles org.openhab.binding.mihome @@ -1126,6 +1141,11 @@ org.openhab.binding.plugwise ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.plugwiseha + ${project.version} + org.openhab.addons.bundles org.openhab.binding.powermax @@ -1216,6 +1236,11 @@ org.openhab.binding.satel ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.semsportal + ${project.version} + org.openhab.addons.bundles org.openhab.binding.senechome diff --git a/bundles/org.openhab.automation.groovyscripting/README.md b/bundles/org.openhab.automation.groovyscripting/README.md index b9b7511f04424..6f1f78721554f 100644 --- a/bundles/org.openhab.automation.groovyscripting/README.md +++ b/bundles/org.openhab.automation.groovyscripting/README.md @@ -1,6 +1,6 @@ # Groovy Scripting -This add-on provides support for [Groovy](https://groovy-lang.org/) 3.0.7 that can be used as a scripting language within automation rules and which eliminates the need to manually install Groovy. +This add-on provides support for [Groovy](https://groovy-lang.org/) 3.0.8 that can be used as a scripting language within automation rules and which eliminates the need to manually install Groovy. ## Creating Groovy Scripts diff --git a/bundles/org.openhab.automation.groovyscripting/pom.xml b/bundles/org.openhab.automation.groovyscripting/pom.xml index ead53f098b5a7..ebf0830b00711 100644 --- a/bundles/org.openhab.automation.groovyscripting/pom.xml +++ b/bundles/org.openhab.automation.groovyscripting/pom.xml @@ -16,7 +16,7 @@ com.ibm.icu.*;resolution:=optional,groovy.runtime.metaclass;resolution:=optional,groovyjarjarantlr4.stringtemplate;resolution:=optional,org.abego.treelayout.*;resolution:=optional,org.apache.ivy.*;resolution:=optional,org.stringtemplate.v4.*;resolution:=optional - 3.0.7 + 3.0.8 diff --git a/bundles/org.openhab.automation.pidcontroller/README.md b/bundles/org.openhab.automation.pidcontroller/README.md index 1630ef814ee17..d0d19702ca30a 100644 --- a/bundles/org.openhab.automation.pidcontroller/README.md +++ b/bundles/org.openhab.automation.pidcontroller/README.md @@ -14,7 +14,12 @@ A PID controller can be used for closed-loop controls. For example: ## Modules -The PID controller can be used in openHAB's [rule engine](https://www.openhab.org/docs/configuration/rules-dsl.html). This automation provides a trigger and an action module. +The PID controller can be used in openHAB's [rule engine](https://www.openhab.org/docs/configuration/rules-dsl.html). +This automation provides a trigger module ("PID controller triggers"). +The return value is used to feed the Action module "Item Action" aka "send a command", which controls the actuator. + +To configure a rule, you need to add a Trigger ("PID controller triggers") and an Action ("Item Action"). +Select the Item you like to control in the "Item Action" and leave the command empty. ### Trigger @@ -32,27 +37,18 @@ This is then transferred to the action module. | `kdTimeConstant` | Decimal | D-T1: [Derivative Gain Time Constant](#derivative-time-constant-d-t1-parameter) in sec. | Y | | `commandItem` | String | Send a String "RESET" to this item to reset the I and the D part to 0. | N | | `loopTime` | Decimal | The interval the output value will be updated in milliseconds. Note: the output will also be updated when the input value or the setpoint changes. | Y | - +| `pInspector` | Item | Name of the debug Item for the current P part | N | +| `iInspector` | Item | Name of the debug Item for the current I part | N | +| `dInspector` | Item | Name of the debug Item for the current D part | N | +| `eInspector` | Item | Name of the debug Item for the current regulation difference (error) | N | The `loopTime` should be max a tenth of the system response. E.g. the heating needs 10 min to heat up the room, the loop time should be max 1 min. Lower values won't harm, but need more calculation resources. -### Action - -This module writes the PID controller's output value into the `output` Item and provides debugging abilities. - -| Name | Type | Description | Required | -|--------------|------|----------------------------------------------------------------------|----------| -| `output` | Item | Name of the output Item (e.g. the valve actuator 0-100%) | Y | -| `pInspector` | Item | Name of the debug Item for the current P part | N | -| `iInspector` | Item | Name of the debug Item for the current I part | N | -| `dInspector` | Item | Name of the debug Item for the current D part | N | -| `eInspector` | Item | Name of the debug Item for the current regulation difference (error) | N | - You can view the internal P, I and D parts of the controller with the inspector Items. These values are useful when tuning the controller. -They are updated everytime the output is updated. +They are updated every time the output is updated. ## Proportional (P) Gain Parameter @@ -112,8 +108,8 @@ This results in quite reasonable working systems in most cases. So, this will be described in the following. To be able to proceed with this method, you need to visualize the input and the output value of the PID controller over time. -It's also good to visualize the individual P, I and D parts (these are forming the output value) via the inspector Items. -The visualization can be done by the analyze function in Main UI or by adding a persistence and use Grafana for example. +It's also good to visualize the individual P, I and D parts (these are forming the output value) via the inspector items. +The visualization could be done by adding a persistence and use Grafana for example. After you added a [Rule](https://www.openhab.org/docs/configuration/rules-dsl.html) with above trigger and action module and configured those, proceed with the following steps: diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java index a3f21fac4b856..fe13aeea7050c 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/PIDControllerConstants.java @@ -36,5 +36,5 @@ public class PIDControllerConstants { public static final String I_INSPECTOR = "iInspector"; public static final String D_INSPECTOR = "dInspector"; public static final String E_INSPECTOR = "eInspector"; - public static final String OUTPUT = "output"; + public static final String COMMAND = "command"; } diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java index f8817bd6abd6f..057490c3a691a 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/factory/PIDControllerModuleHandlerFactory.java @@ -17,9 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.automation.pidcontroller.internal.handler.PIDControllerActionHandler; import org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler; -import org.openhab.core.automation.Action; import org.openhab.core.automation.Module; import org.openhab.core.automation.Trigger; import org.openhab.core.automation.handler.BaseModuleHandlerFactory; @@ -39,8 +37,7 @@ @Component(service = ModuleHandlerFactory.class, configurationPid = "action.pidcontroller") @NonNullByDefault public class PIDControllerModuleHandlerFactory extends BaseModuleHandlerFactory { - private static final Collection TYPES = Set.of(PIDControllerTriggerHandler.MODULE_TYPE_ID, - PIDControllerActionHandler.MODULE_TYPE_ID); + private static final Collection TYPES = Set.of(PIDControllerTriggerHandler.MODULE_TYPE_ID); private ItemRegistry itemRegistry; private EventPublisher eventPublisher; private BundleContext bundleContext; @@ -63,8 +60,6 @@ public Collection getTypes() { switch (module.getTypeUID()) { case PIDControllerTriggerHandler.MODULE_TYPE_ID: return new PIDControllerTriggerHandler((Trigger) module, itemRegistry, eventPublisher, bundleContext); - case PIDControllerActionHandler.MODULE_TYPE_ID: - return new PIDControllerActionHandler((Action) module, itemRegistry, eventPublisher); } return null; diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerActionHandler.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerActionHandler.java deleted file mode 100644 index de9c7030ca831..0000000000000 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerActionHandler.java +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.automation.pidcontroller.internal.handler; - -import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.AUTOMATION_NAME; - -import java.math.BigDecimal; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.automation.Action; -import org.openhab.core.automation.handler.ActionHandler; -import org.openhab.core.automation.handler.BaseModuleHandler; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.events.EventPublisher; -import org.openhab.core.items.ItemRegistry; -import org.openhab.core.items.events.ItemCommandEvent; -import org.openhab.core.items.events.ItemEventFactory; -import org.openhab.core.library.types.DecimalType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * - * @author Hilbrand Bouwkamp - Initial Contribution - * @author Fabian Wolter - Add PID debugging items - */ -@NonNullByDefault -public class PIDControllerActionHandler extends BaseModuleHandler implements ActionHandler { - public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".action"; - - private final Logger logger = LoggerFactory.getLogger(PIDControllerActionHandler.class); - - private ItemRegistry itemRegistry; - private EventPublisher eventPublisher; - - public PIDControllerActionHandler(Action module, ItemRegistry itemRegistry, EventPublisher eventPublisher) { - super(module); - this.itemRegistry = itemRegistry; - this.eventPublisher = eventPublisher; - } - - @Override - public @Nullable Map execute(Map context) { - final Configuration configuration = module.getConfiguration(); - - context.forEach((k, v) -> { - // Remove triggername from key to get raw trigger param - String itemKey = k.substring(k.lastIndexOf('.') + 1); - String itemName = (String) configuration.get(itemKey); - - if (itemName == null || itemName.isBlank()) { - // try original key name (.) - itemName = (String) configuration.get(k); - if (itemName == null || itemName.isBlank()) { - return; - } - } - if (v instanceof BigDecimal) { - final BigDecimal command = (BigDecimal) v; - final DecimalType outputValue = new DecimalType(command); - final ItemCommandEvent itemCommandEvent = ItemEventFactory.createCommandEvent(itemName, outputValue); - - eventPublisher.post(itemCommandEvent); - } else { - logger.warn( - "Command was not posted because either the configuration was not correct or a service was missing: ItemName: {}, Command: {}, eventPublisher: {}, ItemRegistry: {}", - itemName, v, eventPublisher, itemRegistry); - } - }); - return null; - } -} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java index ef61c4bf6e2be..a3de00be26596 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/handler/PIDControllerTriggerHandler.java @@ -15,14 +15,10 @@ import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.*; import java.math.BigDecimal; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -32,7 +28,6 @@ import org.openhab.core.automation.Trigger; import org.openhab.core.automation.handler.BaseTriggerModuleHandler; import org.openhab.core.automation.handler.TriggerHandlerCallback; -import org.openhab.core.common.NamedThreadFactory; import org.openhab.core.config.core.Configuration; import org.openhab.core.events.Event; import org.openhab.core.events.EventFilter; @@ -44,6 +39,7 @@ import org.openhab.core.items.events.ItemEventFactory; import org.openhab.core.items.events.ItemStateChangedEvent; import org.openhab.core.items.events.ItemStateEvent; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; @@ -63,18 +59,19 @@ public class PIDControllerTriggerHandler extends BaseTriggerModuleHandler implem public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".trigger"; private static final Set SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE, ItemStateChangedEvent.TYPE); private final Logger logger = LoggerFactory.getLogger(PIDControllerTriggerHandler.class); - private final ScheduledExecutorService scheduler = Executors - .newSingleThreadScheduledExecutor(new NamedThreadFactory("automation-" + AUTOMATION_NAME, true)); private final ServiceRegistration eventSubscriberRegistration; private final PIDController controller; private final int loopTimeMs; - private @Nullable ScheduledFuture controllerjob; private long previousTimeMs = System.currentTimeMillis(); private Item inputItem; private Item setpointItem; private Optional commandTopic; private EventFilter eventFilter; private EventPublisher eventPublisher; + private @Nullable String pInspector; + private @Nullable String iInspector; + private @Nullable String dInspector; + private @Nullable String eInspector; public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, EventPublisher eventPublisher, BundleContext bundleContext) { @@ -109,6 +106,10 @@ public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, Ev double kiAdjuster = getDoubleFromConfig(config, CONFIG_KI_GAIN); double kdAdjuster = getDoubleFromConfig(config, CONFIG_KD_GAIN); double kdTimeConstant = getDoubleFromConfig(config, CONFIG_KD_TIMECONSTANT); + pInspector = (String) config.get(P_INSPECTOR); + iInspector = (String) config.get(I_INSPECTOR); + dInspector = (String) config.get(D_INSPECTOR); + eInspector = (String) config.get(E_INSPECTOR); loopTimeMs = ((BigDecimal) requireNonNull(config.get(CONFIG_LOOP_TIME), CONFIG_LOOP_TIME + " is not set")) .intValue(); @@ -118,17 +119,21 @@ public PIDControllerTriggerHandler(Trigger module, ItemRegistry itemRegistry, Ev eventFilter = event -> { String topic = event.getTopic(); - return topic.equals("openhab/items/" + inputItemName + "/state") - || topic.equals("openhab/items/" + inputItemName + "/statechanged") - || topic.equals("openhab/items/" + setpointItemName + "/statechanged") + return ("openhab/items/" + inputItemName + "/state").equals(topic) + || ("openhab/items/" + inputItemName + "/statechanged").equals(topic) + || ("openhab/items/" + setpointItemName + "/statechanged").equals(topic) || commandTopic.map(t -> topic.equals(t)).orElse(false); }; eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null); eventPublisher.post(ItemEventFactory.createCommandEvent(inputItemName, RefreshType.REFRESH)); + } - controllerjob = scheduler.scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS); + @Override + public void setCallback(ModuleHandlerCallback callback) { + super.setCallback(callback); + getCallback().getScheduler().scheduleWithFixedDelay(this::calculate, 0, loopTimeMs, TimeUnit.MILLISECONDS); } private T requireNonNull(T obj, String message) { @@ -165,24 +170,27 @@ private void calculate() { PIDOutputDTO output = controller.calculate(input, setpoint, now - previousTimeMs, loopTimeMs); previousTimeMs = now; - Map outputs = new HashMap<>(); + updateItem(pInspector, output.getProportionalPart()); + updateItem(iInspector, output.getIntegralPart()); + updateItem(dInspector, output.getDerivativePart()); + updateItem(eInspector, output.getError()); + + getCallback().triggered(module, Map.of(COMMAND, new DecimalType(output.getOutput()))); + } - putBigDecimal(outputs, OUTPUT, output.getOutput()); - putBigDecimal(outputs, P_INSPECTOR, output.getProportionalPart()); - putBigDecimal(outputs, I_INSPECTOR, output.getIntegralPart()); - putBigDecimal(outputs, D_INSPECTOR, output.getDerivativePart()); - putBigDecimal(outputs, E_INSPECTOR, output.getError()); + private void updateItem(@Nullable String itemName, double value) { + if (itemName != null) { + eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, new DecimalType(value))); + } + } + private TriggerHandlerCallback getCallback() { ModuleHandlerCallback localCallback = callback; if (localCallback != null && localCallback instanceof TriggerHandlerCallback) { - ((TriggerHandlerCallback) localCallback).triggered(module, outputs); - } else { - logger.warn("No callback set"); + return (TriggerHandlerCallback) localCallback; } - } - private void putBigDecimal(Map map, String key, double value) { - map.put(key, BigDecimal.valueOf(value)); + throw new IllegalStateException("The module callback is not set"); } private double getItemValueAsNumber(Item item) throws PIDException { @@ -191,7 +199,7 @@ private double getItemValueAsNumber(Item item) throws PIDException { if (setpointState instanceof Number) { double doubleValue = ((Number) setpointState).doubleValue(); - if (Double.isFinite(doubleValue)) { + if (Double.isFinite(doubleValue) && !Double.isNaN(doubleValue)) { return doubleValue; } } else if (setpointState instanceof StringType) { @@ -237,13 +245,6 @@ public Set getSubscribedEventTypes() { public void dispose() { eventSubscriberRegistration.unregister(); - ScheduledFuture localControllerjob = controllerjob; - if (localControllerjob != null) { - localControllerjob.cancel(true); - } - - scheduler.shutdown(); - super.dispose(); } } diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java index 2786cd16734b3..0de1698b6a66d 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/template/PIDControllerRuleTemplate.java @@ -14,15 +14,11 @@ import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.UUID; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.automation.pidcontroller.internal.PIDControllerConstants; -import org.openhab.automation.pidcontroller.internal.handler.PIDControllerActionHandler; import org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler; -import org.openhab.automation.pidcontroller.internal.type.PIDControllerActionType; import org.openhab.core.automation.Action; import org.openhab.core.automation.Condition; import org.openhab.core.automation.Trigger; @@ -45,15 +41,8 @@ public static PIDControllerRuleTemplate initialize() { final List triggers = List.of(ModuleBuilder.createTrigger().withId(triggerId) .withTypeUID(PIDControllerTriggerHandler.MODULE_TYPE_ID).withLabel("PID Controller Trigger").build()); - final Map actionInputs = Map.of(PIDControllerActionType.INPUT, - triggerId + "." + PIDControllerConstants.OUTPUT); - - final List actions = List.of(ModuleBuilder.createAction().withId(UUID.randomUUID().toString()) - .withTypeUID(PIDControllerActionHandler.MODULE_TYPE_ID).withLabel("PID Controller Action") - .withInputs(actionInputs).build()); - - return new PIDControllerRuleTemplate(Set.of("PID Controller"), triggers, Collections.emptyList(), actions, - Collections.emptyList()); + return new PIDControllerRuleTemplate(Set.of("PID Controller"), triggers, Collections.emptyList(), + Collections.emptyList(), Collections.emptyList()); } public PIDControllerRuleTemplate(Set tags, List triggers, List conditions, diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerActionType.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerActionType.java deleted file mode 100644 index 44054d5699070..0000000000000 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerActionType.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.automation.pidcontroller.internal.type; - -import static org.openhab.automation.pidcontroller.internal.PIDControllerConstants.*; - -import java.math.BigDecimal; -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.automation.pidcontroller.internal.handler.PIDControllerActionHandler; -import org.openhab.core.automation.Visibility; -import org.openhab.core.automation.type.ActionType; -import org.openhab.core.automation.type.Input; -import org.openhab.core.config.core.ConfigDescriptionParameter; -import org.openhab.core.config.core.ConfigDescriptionParameter.Type; -import org.openhab.core.config.core.ConfigDescriptionParameterBuilder; - -/** - * - * @author Hilbrand Bouwkamp - Initial Contribution - */ -@NonNullByDefault -public class PIDControllerActionType extends ActionType { - public static final String INPUT = "input"; - - public static PIDControllerActionType initialize() { - final ConfigDescriptionParameter outputItem = ConfigDescriptionParameterBuilder.create(OUTPUT, Type.TEXT) - .withRequired(true).withMultiple(false).withContext("item").withLabel("Output Item") - .withDescription("Item to send output").build(); - final ConfigDescriptionParameter pInspectorItem = ConfigDescriptionParameterBuilder - .create(P_INSPECTOR, Type.TEXT).withRequired(false).withMultiple(false).withContext("item") - .withLabel("P Inspector Item").withDescription("Item for debugging the P part").build(); - final ConfigDescriptionParameter iInspectorItem = ConfigDescriptionParameterBuilder - .create(I_INSPECTOR, Type.TEXT).withRequired(false).withMultiple(false).withContext("item") - .withLabel("I Inspector Item").withDescription("Item for debugging the I part").build(); - final ConfigDescriptionParameter dInspectorItem = ConfigDescriptionParameterBuilder - .create(D_INSPECTOR, Type.TEXT).withRequired(false).withMultiple(false).withContext("item") - .withLabel("D Inspector Item").withDescription("Item for debugging the D part").build(); - final ConfigDescriptionParameter eInspectorItem = ConfigDescriptionParameterBuilder - .create(E_INSPECTOR, Type.TEXT).withRequired(false).withMultiple(false).withContext("item") - .withLabel("Error Inspector Item").withDescription("Item for debugging the error value").build(); - - List config = List.of(outputItem, pInspectorItem, iInspectorItem, dInspectorItem, - eInspectorItem); - - List inputs = List.of(createInput(INPUT), createInput(P_INSPECTOR), createInput(I_INSPECTOR), - createInput(D_INSPECTOR), createInput(E_INSPECTOR)); - - return new PIDControllerActionType(config, inputs); - } - - private static Input createInput(String name) { - return new Input(name, BigDecimal.class.getName()); - } - - public PIDControllerActionType(List configDescriptions, List inputs) { - super(PIDControllerActionHandler.MODULE_TYPE_ID, configDescriptions, "calculate PID output", null, null, - Visibility.VISIBLE, inputs, null); - } -} diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java index 7db95d937daa8..6da1faaf41307 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerModuleTypeProvider.java @@ -19,7 +19,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.automation.pidcontroller.internal.handler.PIDControllerActionHandler; import org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler; import org.openhab.core.automation.type.ModuleType; import org.openhab.core.automation.type.ModuleTypeProvider; @@ -33,9 +32,8 @@ @Component @NonNullByDefault public class PIDControllerModuleTypeProvider implements ModuleTypeProvider { - private static final Map PROVIDED_MODULE_TYPES = Map.of( - PIDControllerActionHandler.MODULE_TYPE_ID, PIDControllerActionType.initialize(), - PIDControllerTriggerHandler.MODULE_TYPE_ID, PIDControllerTriggerType.initialize()); + private static final Map PROVIDED_MODULE_TYPES = Map + .of(PIDControllerTriggerHandler.MODULE_TYPE_ID, PIDControllerTriggerType.initialize()); @SuppressWarnings("unchecked") @Override diff --git a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java index 0909c90abcb04..2fd11bfecacf2 100644 --- a/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java +++ b/bundles/org.openhab.automation.pidcontroller/src/main/java/org/openhab/automation/pidcontroller/internal/type/PIDControllerTriggerType.java @@ -17,6 +17,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.automation.pidcontroller.internal.handler.PIDControllerTriggerHandler; @@ -30,24 +31,26 @@ /** * * @author Hilbrand Bouwkamp - Initial Contribution + * @author Fabian Wolter - Add inspector Items for debugging */ @NonNullByDefault public class PIDControllerTriggerType extends TriggerType { private static final String DEFAULT_LOOPTIME_MS = "1000"; + private static final String ITEM = "item"; public static PIDControllerTriggerType initialize() { List configDescriptions = new ArrayList<>(); configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_INPUT_ITEM, Type.TEXT) // .withRequired(true) // .withMultiple(false) // - .withContext("item") // + .withContext(ITEM) // .withLabel("Input Item") // .withDescription("Item to monitor") // .build()); configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_SETPOINT_ITEM, Type.TEXT) // .withRequired(true) // .withMultiple(false) // - .withContext("item") // + .withContext(ITEM) // .withLabel("Setpoint") // .withDescription("Targeted setpoint") // .build()); @@ -83,13 +86,6 @@ public static PIDControllerTriggerType initialize() { .withDescription("Slows the rate of change of the D part (T1) in seconds.") // .withUnit("s") // .build()); - configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_COMMAND_ITEM, Type.TEXT) // - .withRequired(false) // - .withMultiple(false) // - .withContext("item") // - .withLabel("Command Item") // - .withDescription("You can send String commands to this Item like \"RESET\".") // - .build()); configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_LOOP_TIME, Type.DECIMAL) // .withRequired(true) // .withMultiple(false) // @@ -98,20 +94,37 @@ public static PIDControllerTriggerType initialize() { .withDescription("The interval the output value is updated in ms") // .withUnit("ms") // .build()); - Output output = new Output(OUTPUT, BigDecimal.class.getName(), "Output", "Output value of the PID Controller", - null, null, null); - Output pInspector = new Output(P_INSPECTOR, BigDecimal.class.getName(), "P Inspector", - "Current P value of the pid controller", null, null, null); - Output iInspector = new Output(I_INSPECTOR, BigDecimal.class.getName(), "I Inspector", - "Current I value of the pid controller", null, null, null); - Output dInspector = new Output(D_INSPECTOR, BigDecimal.class.getName(), "D Inspector", - "Current D value of the pid controller", null, null, null); - Output eInspector = new Output(E_INSPECTOR, BigDecimal.class.getName(), "Error Value Inspector", - "Current error value of the pid controller", null, null, null); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(P_INSPECTOR, Type.TEXT) // + .withRequired(false) // + .withMultiple(false) // + .withContext(ITEM) // + .withLabel("P Inspector Item") // + .withDescription("Item for debugging the P part") // + .build()); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(I_INSPECTOR, Type.TEXT) // + .withRequired(false) // + .withMultiple(false) // + .withContext(ITEM) // + .withLabel("I Inspector Item") // + .withDescription("Item for debugging the I part") // + .build()); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(D_INSPECTOR, Type.TEXT) // + .withRequired(false).withMultiple(false) // + .withContext(ITEM) // + .withLabel("D Inspector Item") // + .withDescription("Item for debugging the D part") // + .build()); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(E_INSPECTOR, Type.TEXT) // + .withRequired(false).withMultiple(false) // + .withContext(ITEM) // + .withLabel("Error Inspector Item") // + .withDescription("Item for debugging the error value") // + .build()); - List outputs = List.of(output, pInspector, iInspector, dInspector, eInspector); + Output output = new Output(COMMAND, BigDecimal.class.getName(), "Output", "Output value of the PID Controller", + Set.of("command"), null, null); - return new PIDControllerTriggerType(configDescriptions, outputs); + return new PIDControllerTriggerType(configDescriptions, List.of(output)); } public PIDControllerTriggerType(List configDescriptions, List outputs) { diff --git a/bundles/org.openhab.automation.pwm/NOTICE b/bundles/org.openhab.automation.pwm/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/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.automation.pwm/README.md b/bundles/org.openhab.automation.pwm/README.md new file mode 100644 index 0000000000000..840b19df8715a --- /dev/null +++ b/bundles/org.openhab.automation.pwm/README.md @@ -0,0 +1,62 @@ +# Pulse Width Modulation (PWM) Automation + +This automation module implements [Pulse Width Modulation (PWM)](https://en.wikipedia.org/wiki/Pulse-width_modulation). + +PWM can be used to control actuators continuously from 0 to 100% that only support ON/OFF commands. +E.g. valves or heating burners. +It accomplishes that by switching the actuator on and off with a fixed interval. +The higher the control percentage (duty cycle), the longer the ON phase. + +Example: If you have an interval of 10 sec and the duty cycle is 30%, the output is ON for 3 sec and OFF for 7 sec. + +This module is **unsuitable** for controlling LED lights as the high PWM frequency can't be met. + +> Note: The module starts to work only if the duty cycle has been updated at least once. + +## Modules + +The PWM module can be used in openHAB's [rule engine](https://www.openhab.org/docs/configuration/rules-dsl.html). + +This automation provides a trigger module ("PWM triggers") with one input Item: `dutycycleItem` (0-100%). +The module calculates the ON/OFF state and returns it. +The return value is used to feed the Action module "Item Action" aka "send a command", which controls the actuator. + +To configure a rule, you need to add a Trigger ("PWM triggers") and an Action ("Item Action"). +Select the Item you like to control in the "Item Action" and leave the command empty. + +### Trigger + +| Name | Type | Description | Required | +|-----------------|---------|----------------------------------------------------------------------------------------------|----------| +| `dutycycleItem` | Item | The Item (PercentType) to read the duty cycle from | Yes | +| `interval` | Decimal | The constant interval in which the output is switch ON and OFF again in sec. | Yes | +| `minDutyCycle` | Decimal | Any duty cycle below this value will be increased to this value | No | +| `maxDutycycle` | Decimal | Any duty cycle above this value will be decreased to this value | No | +| `deadManSwitch` | Decimal | The output will be switched off, when the duty cycle is not updated within this time (in ms) | No | + +The duty cycle can be limited via the parameters `minDutycycle` and `maxDutyCycle`. +This is helpful if you need to maintain a minimum time between the switching of the output. +This is necessary for example for heating burners, which may not be switched on for very short times. +The on time is than increased to `minDutycycle`. +In this case one should also set a max duty cycle to prevent short off times. +It makes sense to apply these symmetrically e.g. 10%/90% or 20%/80%. + +If the duty cycle is 0% or 100%, the min/max parameters are ignored and the output is switched ON or OFF continuously. + +If the duty cycle Item is not updated within the dead-man switch timeout, the output is switched off, regardless of the current duty cycle. +The function can be used to save energy if the source of the duty cycle died for whatever reason and doesn't update the value anymore. +When the duty cycle is updated again, the module returns to normal operation. + +> Note: The min/max ON/OFF times set via `minDutycycle` and `maxDutycycle` are not met if the dead-man switch triggers and recovers fast. + +## Control Algorithm + +This module is designed to respond fast to duty cycle changes, but at the same time maintain a constant interval and also the min/max ON/OFF parameters. +For that reason, the module might seem to act peculiarly in some cases: + +- When the output is ON and the duty cycle is decreased, the output might switch off immediately, if applicable. +Example: The interval is 10 sec and the current duty cycle is 80%. +When the duty cycle is decreased to 20%, the output would switch off immediately, if it has been already ON for more than 2 sec. +- When the duty cycle is 0% for a short interval and then increased again, the output will only switch on when the new interval starts. +- When the duty cycle is 0% or 100% for more than a whole interval, a new interval will start as soon as the duty cycle is updated to a value other than 0%, respective 100%. +- The module starts to work only if the duty cycle Item has been updated at least once. diff --git a/bundles/org.openhab.automation.pwm/doc/statemachine.odg b/bundles/org.openhab.automation.pwm/doc/statemachine.odg new file mode 100644 index 0000000000000..be28e78e32ba2 Binary files /dev/null and b/bundles/org.openhab.automation.pwm/doc/statemachine.odg differ diff --git a/bundles/org.openhab.automation.pwm/doc/statemachine.png b/bundles/org.openhab.automation.pwm/doc/statemachine.png new file mode 100644 index 0000000000000..aa99d12bd492f Binary files /dev/null and b/bundles/org.openhab.automation.pwm/doc/statemachine.png differ diff --git a/bundles/org.openhab.automation.pwm/pom.xml b/bundles/org.openhab.automation.pwm/pom.xml new file mode 100644 index 0000000000000..d972adb157105 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.automation.pwm + + openHAB Add-ons :: Bundles :: Automation :: PWM + + diff --git a/bundles/org.openhab.automation.pwm/src/main/feature/feature.xml b/bundles/org.openhab.automation.pwm/src/main/feature/feature.xml new file mode 100644 index 0000000000000..212e8c27b981d --- /dev/null +++ b/bundles/org.openhab.automation.pwm/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.automation.pwm/${project.version} + + diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/PWMConstants.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/PWMConstants.java new file mode 100644 index 0000000000000..e2072322a7939 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/PWMConstants.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Constants for the PWM automation module. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class PWMConstants { + public static final String AUTOMATION_NAME = "pwm"; + + public static final String CONFIG_DUTY_CYCLE_ITEM = "dutycycleItem"; + public static final String CONFIG_PERIOD = "interval"; + public static final String CONFIG_MIN_DUTYCYCLE = "minDutycycle"; + public static final String CONFIG_MAX_DUTYCYCLE = "maxDutycycle"; + public static final String CONFIG_COMMAND_ITEM = "command"; + public static final String CONFIG_DEAD_MAN_SWITCH = "deadManSwitch"; + public static final String CONFIG_OUTPUT_ITEM = "outputItem"; + public static final String INPUT = "input"; + public static final String OUTPUT = "command"; +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/PWMException.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/PWMException.java new file mode 100644 index 0000000000000..8b2f86b90a5ed --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/PWMException.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Common exception for the PWM automation module. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class PWMException extends Exception { + private static final long serialVersionUID = -3029834022610530982L; + + public PWMException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/factory/PWMModuleHandlerFactory.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/factory/PWMModuleHandlerFactory.java new file mode 100644 index 0000000000000..87e54e0bb8693 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/factory/PWMModuleHandlerFactory.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.factory; + +import java.util.Collection; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.pwm.internal.handler.PWMTriggerHandler; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseModuleHandlerFactory; +import org.openhab.core.automation.handler.ModuleHandler; +import org.openhab.core.automation.handler.ModuleHandlerFactory; +import org.openhab.core.items.ItemRegistry; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Factory for the PWM automation module. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +@Component(service = ModuleHandlerFactory.class, configurationPid = "automation.pwm") +public class PWMModuleHandlerFactory extends BaseModuleHandlerFactory { + private static final Collection TYPES = Set.of(PWMTriggerHandler.MODULE_TYPE_ID); + private ItemRegistry itemRegistry; + private BundleContext bundleContext; + + @Activate + public PWMModuleHandlerFactory(@Reference ItemRegistry itemRegistry, BundleContext bundleContext) { + this.itemRegistry = itemRegistry; + this.bundleContext = bundleContext; + } + + @Override + public Collection getTypes() { + return TYPES; + } + + @Override + protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) { + switch (module.getTypeUID()) { + case PWMTriggerHandler.MODULE_TYPE_ID: + return new PWMTriggerHandler((Trigger) module, itemRegistry, bundleContext); + } + + return null; + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/PWMTriggerHandler.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/PWMTriggerHandler.java new file mode 100644 index 0000000000000..f5c1619841d64 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/PWMTriggerHandler.java @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler; + +import static org.openhab.automation.pwm.internal.PWMConstants.*; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.Objects; +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.openhab.automation.pwm.internal.PWMException; +import org.openhab.automation.pwm.internal.handler.state.StateMachine; +import org.openhab.core.automation.ModuleHandlerCallback; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.handler.BaseTriggerModuleHandler; +import org.openhab.core.automation.handler.TriggerHandlerCallback; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.events.Event; +import org.openhab.core.events.EventFilter; +import org.openhab.core.events.EventSubscriber; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.events.ItemStateEvent; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents a Trigger module in the rules engine. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class PWMTriggerHandler extends BaseTriggerModuleHandler implements EventSubscriber { + public static final String MODULE_TYPE_ID = AUTOMATION_NAME + ".trigger"; + private static final Set SUBSCRIBED_EVENT_TYPES = Set.of(ItemStateEvent.TYPE); + private final Logger logger = LoggerFactory.getLogger(PWMTriggerHandler.class); + private final BundleContext bundleContext; + private final EventFilter eventFilter; + private final Optional minDutyCycle; + private final Optional maxDutyCycle; + private final Optional deadManSwitchTimeoutMs; + private final Item dutyCycleItem; + private @Nullable ServiceRegistration eventSubscriberRegistration; + private @Nullable ScheduledFuture deadMeanSwitchTimer; + private @Nullable StateMachine stateMachine; + + public PWMTriggerHandler(Trigger module, ItemRegistry itemRegistry, BundleContext bundleContext) { + super(module); + this.bundleContext = bundleContext; + + Configuration config = module.getConfiguration(); + + String dutycycleItemName = (String) Objects.requireNonNull(config.get(CONFIG_DUTY_CYCLE_ITEM), + "DutyCycle item is not set"); + + minDutyCycle = getOptionalDoubleFromConfig(config, CONFIG_MIN_DUTYCYCLE); + maxDutyCycle = getOptionalDoubleFromConfig(config, CONFIG_MAX_DUTYCYCLE); + deadManSwitchTimeoutMs = getOptionalDoubleFromConfig(config, CONFIG_DEAD_MAN_SWITCH); + + try { + dutyCycleItem = itemRegistry.getItem(dutycycleItemName); + } catch (ItemNotFoundException e) { + throw new IllegalArgumentException("Dutycycle item not found: " + dutycycleItemName, e); + } + + eventFilter = event -> event.getTopic().equals("openhab/items/" + dutycycleItemName + "/state"); + } + + @Override + public void setCallback(ModuleHandlerCallback callback) { + super.setCallback(callback); + + double periodSec = getDoubleFromConfig(module.getConfiguration(), CONFIG_PERIOD); + stateMachine = new StateMachine(getCallback().getScheduler(), this::setOutput, (long) (periodSec * 1000)); + + eventSubscriberRegistration = bundleContext.registerService(EventSubscriber.class.getName(), this, null); + } + + private double getDoubleFromConfig(Configuration config, String key) { + return ((BigDecimal) Objects.requireNonNull(config.get(key), key + " is not set")).doubleValue(); + } + + private Optional getOptionalDoubleFromConfig(Configuration config, String key) { + Object o = config.get(key); + + if (o instanceof BigDecimal) { + return Optional.of(((BigDecimal) o).doubleValue()); + } + + return Optional.empty(); + } + + @Override + public void receive(Event event) { + if (!(event instanceof ItemStateEvent)) { + return; + } + + ItemStateEvent changedEvent = (ItemStateEvent) event; + synchronized (this) { + try { + double newDutycycle = getDutyCycleValueInPercent(changedEvent.getItemState()); + double newDutycycleBeforeLimit = newDutycycle; + + restartDeadManSwitchTimer(); + + // set duty cycle to min duty cycle if it is smaller than min duty cycle + // set duty cycle to 0% if it is 0%, regardless of the min duty cycle + final double newDutyCycleFinal1 = newDutycycle; + newDutycycle = minDutyCycle.map(minDutycycle -> { + if (Math.round(newDutyCycleFinal1) <= 0) { + return 0d; + } else { + return Math.max(minDutycycle, newDutyCycleFinal1); + } + }).orElse(newDutycycle); + + // set duty cycle to 100% if the current duty cycle is larger than the max duty cycle + final double newDutyCycleFinal2 = newDutycycle; + newDutycycle = maxDutyCycle.map(maxDutycycle -> { + if (Math.round(newDutyCycleFinal2) >= maxDutycycle) { + return 100d; + } else { + return newDutyCycleFinal2; + } + }).orElse(newDutycycle); + + logger.debug("Received new duty cycle: {} {}", newDutycycleBeforeLimit, + newDutycycle != newDutycycleBeforeLimit ? "Limited to: " + newDutycycle : ""); + + StateMachine localStateMachine = stateMachine; + if (localStateMachine != null) { + localStateMachine.setDutycycle(newDutycycle); + } else { + logger.debug("Initialization not finished"); + } + } catch (PWMException e) { + logger.warn("{}", e.getMessage()); + } + } + } + + private void restartDeadManSwitchTimer() { + ScheduledFuture timer = deadMeanSwitchTimer; + if (timer != null) { + timer.cancel(true); + } + + deadManSwitchTimeoutMs.ifPresent(timeout -> { + deadMeanSwitchTimer = getCallback().getScheduler().schedule(this::activateDeadManSwitch, + timeout.longValue(), TimeUnit.MILLISECONDS); + }); + } + + private void activateDeadManSwitch() { + logger.warn("Dead-man switch activated. Disabling output"); + + StateMachine localStateMachine = stateMachine; + if (localStateMachine != null) { + localStateMachine.stop(); + } + } + + private void setOutput(boolean enable) { + getCallback().triggered(module, Collections.singletonMap(OUTPUT, OnOffType.from(enable))); + } + + private TriggerHandlerCallback getCallback() { + ModuleHandlerCallback localCallback = callback; + if (localCallback != null && localCallback instanceof TriggerHandlerCallback) { + return (TriggerHandlerCallback) localCallback; + } + + throw new IllegalStateException(); + } + + private double getDutyCycleValueInPercent(State state) throws PWMException { + if (state instanceof DecimalType) { + return ((DecimalType) state).doubleValue(); + } else if (state instanceof StringType) { + try { + return Integer.parseInt(state.toString()); + } catch (NumberFormatException e) { + // nothing + } + } else if (state instanceof UnDefType) { + throw new PWMException("Duty cycle item '" + dutyCycleItem.getName() + "' has no valid value"); + } + throw new PWMException("Duty cycle item not of type DecimalType: " + state.getClass().getSimpleName()); + } + + @Override + public Set getSubscribedEventTypes() { + return SUBSCRIBED_EVENT_TYPES; + } + + @Override + public @Nullable EventFilter getEventFilter() { + return eventFilter; + } + + @Override + public void dispose() { + ServiceRegistration localEventSubscriberRegistration = eventSubscriberRegistration; + if (localEventSubscriberRegistration != null) { + localEventSubscriberRegistration.unregister(); + } + + StateMachine localStateMachine = stateMachine; + if (localStateMachine != null) { + localStateMachine.stop(); + } + + super.dispose(); + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/AlwaysOffState.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/AlwaysOffState.java new file mode 100644 index 0000000000000..e8e21be7935c2 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/AlwaysOffState.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler.state; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Active when, the duty cycle is 0% for at least a whole period. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class AlwaysOffState extends State { + public AlwaysOffState(StateMachine context) { + super(context); + + controlOutput(false); + } + + @Override + public void dutyCycleChanged() { + if (Math.round(context.getDutycycle()) >= 100) { + nextState(DutycycleHundredState::new); + } else { + nextState(OnState::new); + } + } + + @Override + protected void dutyCycleUpdated() { + // in case we came here by the dead-man switch + if (Math.round(context.getDutycycle()) > 0) { + nextState(OnState::new); + } + } + + @Override + public void dispose() { + // nothing + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/AlwaysOnState.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/AlwaysOnState.java new file mode 100644 index 0000000000000..53d49c0947561 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/AlwaysOnState.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler.state; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Active when, the duty cycle is 100% for at least a whole period. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class AlwaysOnState extends State { + public AlwaysOnState(StateMachine context) { + super(context); + + controlOutput(true); + } + + @Override + public void dutyCycleChanged() { + nextState(OffState::new); + } + + @Override + protected void dutyCycleUpdated() { + // nothing + } + + @Override + public void dispose() { + // nothing + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/DutycycleHundredState.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/DutycycleHundredState.java new file mode 100644 index 0000000000000..121549c42c711 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/DutycycleHundredState.java @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler.state; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Active when, the PWM period ended with a duty cycle set to 100%. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class DutycycleHundredState extends State { + private ScheduledFuture periodTimer; + private @Nullable ScheduledFuture offTimer; + private Instant enabledAt = Instant.now(); + private boolean dutyCycleChanged; + + public DutycycleHundredState(StateMachine context) { + super(context); + + controlOutput(true); + + periodTimer = scheduler.schedule(this::periodEnded, context.getPeriodMs(), TimeUnit.MILLISECONDS); + } + + private void periodEnded() { + long dutycycleRounded = Math.round(context.getDutycycle()); + + if (!dutyCycleChanged && dutycycleRounded <= 0) { + nextState(AlwaysOffState::new); + } else if (!dutyCycleChanged && dutycycleRounded >= 100) { + nextState(AlwaysOnState::new); + } else { + nextState(OnState::new); + } + } + + @Override + public void dutyCycleChanged() { + dutyCycleChanged = true; + + long newOnTimeMs = calculateOnTimeMs(context.getDutycycle()); + long elapsedMs = enabledAt.until(Instant.now(), ChronoUnit.MILLIS); + + if (elapsedMs - newOnTimeMs > 0) { + controlOutput(false); + } else { + ScheduledFuture timer = offTimer; + if (timer != null) { + timer.cancel(false); + } + offTimer = scheduler.schedule(() -> controlOutput(false), newOnTimeMs - elapsedMs, TimeUnit.MILLISECONDS); + } + } + + @Override + protected void dutyCycleUpdated() { + // nothing + } + + @Override + public void dispose() { + periodTimer.cancel(false); + + ScheduledFuture timer = offTimer; + if (timer != null) { + timer.cancel(false); + } + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/DutycycleZeroState.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/DutycycleZeroState.java new file mode 100644 index 0000000000000..59e3a12508a09 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/DutycycleZeroState.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler.state; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Active when, the PWM period ended with a duty cycle set to 0%. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class DutycycleZeroState extends State { + private ScheduledFuture periodTimer; + + public DutycycleZeroState(StateMachine context) { + super(context); + + controlOutput(false); + + periodTimer = scheduler.schedule(this::periodEnded, context.getPeriodMs(), TimeUnit.MILLISECONDS); + } + + private void periodEnded() { + long dutycycleRounded = Math.round(context.getDutycycle()); + + if (dutycycleRounded <= 0) { + nextState(AlwaysOffState::new); + } else if (dutycycleRounded >= 100) { + nextState(DutycycleHundredState::new); + } else { + nextState(OnState::new); + } + } + + @Override + public void dutyCycleChanged() { + // nothing + } + + @Override + protected void dutyCycleUpdated() { + // nothing + } + + @Override + public void dispose() { + periodTimer.cancel(false); + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/OffState.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/OffState.java new file mode 100644 index 0000000000000..0762d2da6cda7 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/OffState.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler.state; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Active when, the output is currently OFF and the duty cycle is between 0% and 100% (exclusively). + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class OffState extends State { + ScheduledFuture offTimer; + + public OffState(StateMachine context) { + super(context); + + controlOutput(false); + + long offTimeMs = context.getPeriodMs() - calculateOnTimeMs(context.getDutycycle()); + offTimer = scheduler.schedule(this::periodEnded, offTimeMs, TimeUnit.MILLISECONDS); + } + + private void periodEnded() { + long dutycycleRounded = Math.round(context.getDutycycle()); + + if (dutycycleRounded <= 0) { + nextState(DutycycleZeroState::new); + } else if (dutycycleRounded >= 100) { + nextState(DutycycleHundredState::new); + } else { + nextState(OnState::new); + } + } + + @Override + public void dutyCycleChanged() { + // nothing + } + + @Override + protected void dutyCycleUpdated() { + // nothing + } + + @Override + public void dispose() { + offTimer.cancel(false); + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/OnState.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/OnState.java new file mode 100644 index 0000000000000..e1c22c24cd30a --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/OnState.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler.state; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Active when, the output is currently ON and the duty cycle is between 0% and 100% (exclusively). + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class OnState extends State { + private @NonNullByDefault({}) ScheduledFuture offTimer; + private Instant enabledAt = Instant.now(); + + public OnState(StateMachine context) { + super(context); + + context.controlOutput(true); + + startOnTimer(calculateOnTimeMs(context.getDutycycle())); + } + + private void startOnTimer(long timeMs) { + offTimer = scheduler.schedule(() -> { + if (Math.round(context.getDutycycle()) >= 100) { + nextState(DutycycleHundredState::new); + } else { + nextState(OffState::new); + } + }, timeMs, TimeUnit.MILLISECONDS); + } + + @Override + public void dutyCycleChanged() { + // end current ON phase prematurely or extend it if the new duty cycle demands it + offTimer.cancel(false); + + long newOnTimeMs = calculateOnTimeMs(context.getDutycycle()); + long elapsedMs = enabledAt.until(Instant.now(), ChronoUnit.MILLIS); + + if (elapsedMs - newOnTimeMs > 0) { + nextState(OffState::new); + } else { + startOnTimer(newOnTimeMs - elapsedMs); + } + } + + @Override + protected void dutyCycleUpdated() { + // nothing + } + + @Override + public void dispose() { + offTimer.cancel(false); + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/State.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/State.java new file mode 100644 index 0000000000000..2bf490b5e9aab --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/State.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler.state; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The base class of all states. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public abstract class State { + private final Logger logger = LoggerFactory.getLogger(State.class); + protected StateMachine context; + protected ScheduledExecutorService scheduler; + + public State(StateMachine context) { + this.context = context; + this.scheduler = context.getScheduler(); + } + + /** + * Invoked when the duty cycle updated and changed. + */ + public abstract void dutyCycleChanged(); + + /** + * Invoked when the duty cycle updated. + */ + protected abstract void dutyCycleUpdated(); + + public abstract void dispose(); + + /** + * Sets a new state in the state machine. + */ + public synchronized void nextState(Function nextState) { + if (context.getState() != this) { // compare identity + return; + } + + context.getState().dispose(); + State newState = nextState.apply(context); + + logger.trace("{} -> {}", context.getState().getClass().getSimpleName(), newState.getClass().getSimpleName()); + + context.setState(newState); + } + + /** + * Calculates the ON duration by the duty cycle. + * + * @param dutyCycleInPercent the duty cycle in percent + * @return the ON duration in ms + */ + protected long calculateOnTimeMs(double dutyCycleInPercent) { + return (long) (context.getPeriodMs() / 100 * dutyCycleInPercent); + } + + /** + * Switches the output on or off. + * + * @param on true, if the output shall be switched on. + */ + protected void controlOutput(boolean on) { + context.controlOutput(on); + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/StateMachine.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/StateMachine.java new file mode 100644 index 0000000000000..47c8454e5dfe8 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/StateMachine.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.handler.state; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The context of all states. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class StateMachine { + private ScheduledExecutorService scheduler; + private Consumer controlOutput; + private State state; + private long periodMs; + private double dutycycle; + + public StateMachine(ScheduledExecutorService scheduler, Consumer controlOutput, long periodMs) { + this.scheduler = scheduler; + this.controlOutput = controlOutput; + this.periodMs = periodMs; + this.state = new AlwaysOffState(this); + } + + public ScheduledExecutorService getScheduler() { + return scheduler; + } + + public void setDutycycle(double newDutycycle) { + if (dutycycle != newDutycycle) { + this.dutycycle = newDutycycle; + state.dutyCycleChanged(); + } + + state.dutyCycleUpdated(); + } + + public double getDutycycle() { + return dutycycle; + } + + public long getPeriodMs() { + return periodMs; + } + + public State getState() { + return state; + } + + public void setState(State current) { + this.state = current; + } + + public void controlOutput(boolean on) { + controlOutput.accept(on); + } + + public void reset() { + state.nextState(OnState::new); + } + + public void stop() { + state.nextState(AlwaysOffState::new); + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/template/PWMRuleTemplate.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/template/PWMRuleTemplate.java new file mode 100644 index 0000000000000..cf715d72c64aa --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/template/PWMRuleTemplate.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.template; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.pwm.internal.PWMConstants; +import org.openhab.automation.pwm.internal.type.PWMTriggerType; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.util.ModuleBuilder; +import org.openhab.core.config.core.ConfigDescriptionParameter; + +/** + * Rule template for the PWM automation module. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class PWMRuleTemplate extends RuleTemplate { + public static final String UID = "PWMRuleTemplate"; + + public static PWMRuleTemplate initialize() { + final String triggerId = UUID.randomUUID().toString(); + + final List triggers = Collections.singletonList(ModuleBuilder.createTrigger().withId(triggerId) + .withTypeUID(PWMTriggerType.UID).withLabel("PWM Trigger").build()); + + final Map actionInputs = new HashMap(); + actionInputs.put(PWMConstants.INPUT, triggerId + "." + PWMConstants.OUTPUT); + + Set tags = new HashSet(); + tags.add("PWM"); + + return new PWMRuleTemplate(tags, triggers, Collections.emptyList(), Collections.emptyList(), + Collections.emptyList()); + } + + public PWMRuleTemplate(Set tags, List triggers, List conditions, List actions, + List configDescriptions) { + super(UID, "PWM", "Template for a PWM rule", tags, triggers, conditions, actions, configDescriptions, + Visibility.VISIBLE); + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/template/PWMTemplateProvider.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/template/PWMTemplateProvider.java new file mode 100644 index 0000000000000..87fc455d9f1a4 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/template/PWMTemplateProvider.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.template; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.template.RuleTemplate; +import org.openhab.core.automation.template.RuleTemplateProvider; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.osgi.service.component.annotations.Component; + +/** + * Rule template provider for the PWM automation module. + * + * @author Fabian Wolter - Initial Contribution + */ +@Component +@NonNullByDefault +public class PWMTemplateProvider implements RuleTemplateProvider { + private final Map providedRuleTemplates = new HashMap(); + + public PWMTemplateProvider() { + providedRuleTemplates.put(PWMRuleTemplate.UID, PWMRuleTemplate.initialize()); + } + + @Override + @Nullable + public RuleTemplate getTemplate(String UID, @Nullable Locale locale) { + return providedRuleTemplates.get(UID); + } + + @Override + public Collection getTemplates(@Nullable Locale locale) { + return providedRuleTemplates.values(); + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + // does nothing because this provider does not change + } + + @Override + public Collection getAll() { + return Collections.unmodifiableCollection(providedRuleTemplates.values()); + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + // does nothing because this provider does not change + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/type/PWMModuleTypeProvider.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/type/PWMModuleTypeProvider.java new file mode 100644 index 0000000000000..2db14925d489d --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/type/PWMModuleTypeProvider.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.type; + +import java.util.Collection; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.automation.pwm.internal.handler.PWMTriggerHandler; +import org.openhab.core.automation.type.ModuleType; +import org.openhab.core.automation.type.ModuleTypeProvider; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.osgi.service.component.annotations.Component; + +/** + * Provides the module types for the rules engine. + * + * @author Fabian Wolter - Initial Contribution + */ +@Component +@NonNullByDefault +public class PWMModuleTypeProvider implements ModuleTypeProvider { + private static final Map PROVIDED_MODULE_TYPES = Map.of(PWMTriggerHandler.MODULE_TYPE_ID, + PWMTriggerType.initialize()); + + @SuppressWarnings("unchecked") + @Override + public T getModuleType(@Nullable String UID, @Nullable Locale locale) { + return (T) PROVIDED_MODULE_TYPES.get(UID); + } + + @SuppressWarnings("unchecked") + @Override + public Collection getModuleTypes(@Nullable Locale locale) { + return (Collection) PROVIDED_MODULE_TYPES.values(); + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + // does nothing because this provider does not change + } + + @Override + public Collection getAll() { + return Collections.unmodifiableCollection(PROVIDED_MODULE_TYPES.values()); + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + // does nothing because this provider does not change + } +} diff --git a/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/type/PWMTriggerType.java b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/type/PWMTriggerType.java new file mode 100644 index 0000000000000..f0859328d6229 --- /dev/null +++ b/bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/type/PWMTriggerType.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2021 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.automation.pwm.internal.type; + +import static org.openhab.automation.pwm.internal.PWMConstants.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.pwm.internal.handler.PWMTriggerHandler; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.type.Output; +import org.openhab.core.automation.type.TriggerType; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.ConfigDescriptionParameter.Type; +import org.openhab.core.config.core.ConfigDescriptionParameterBuilder; +import org.openhab.core.library.types.OnOffType; + +/** + * Creates the configuration for the Trigger module in the rules engine. + * + * @author Fabian Wolter - Initial Contribution + */ +@NonNullByDefault +public class PWMTriggerType extends TriggerType { + public static final String UID = PWMTriggerHandler.MODULE_TYPE_ID; + + public static PWMTriggerType initialize() { + List configDescriptions = new ArrayList<>(); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_DUTY_CYCLE_ITEM, Type.TEXT) // + .withRequired(true) // + .withMultiple(false) // + .withContext("item") // + .withLabel("Dutycycle Item").withDescription("Item to read the current dutycycle from (PercentType)") + .build()); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_PERIOD, Type.DECIMAL) // + .withRequired(true) // + .withMultiple(false) // + .withDefault("600") // + .withLabel("PWM Interval") // + .withUnit("s") // + .withDescription("Duration of the PWM interval in sec.").build()); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_MIN_DUTYCYCLE, Type.DECIMAL) // + .withRequired(false) // + .withMultiple(false) // + .withMinimum(BigDecimal.ZERO) // + .withMaximum(BigDecimal.valueOf(100)) // + .withDefault("0") // + .withLabel("Min Dutycycle") // + .withUnit("%") // + .withDescription("The dutycycle will be min this value").build()); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_MAX_DUTYCYCLE, Type.DECIMAL) // + .withRequired(false) // + .withMultiple(false) // + .withMinimum(BigDecimal.ZERO) // + .withMaximum(BigDecimal.valueOf(100)) // + .withDefault("100") // + .withUnit("%") // + .withLabel("Max Dutycycle") // + .withDescription("The dutycycle will be max this value").build()); + configDescriptions.add(ConfigDescriptionParameterBuilder.create(CONFIG_DEAD_MAN_SWITCH, Type.DECIMAL) // + .withRequired(false) // + .withMultiple(false) // + .withMinimum(BigDecimal.ZERO) // + .withDefault("") // + .withLabel("Dead Man Switch") // + .withUnit("ms") // + .withDescription( + "If the duty cycle Item is not updated within this time (in ms), the output is switched off") + .build()); + + List outputs = Collections.singletonList(new Output(OUTPUT, OnOffType.class.getName(), "Output", + "Output value of the PWM module", Set.of("command"), null, null)); + + return new PWMTriggerType(configDescriptions, outputs); + } + + public PWMTriggerType(List configDescriptions, List outputs) { + super(UID, configDescriptions, "PWM triggers", null, null, Visibility.VISIBLE, outputs); + } +} diff --git a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandler.java b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandler.java index 26bffeb6c002e..0f9bdf21a0bce 100644 --- a/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandler.java +++ b/bundles/org.openhab.binding.ahawastecollection/src/main/java/org/openhab/binding/ahawastecollection/internal/AhaWasteCollectionHandler.java @@ -175,11 +175,12 @@ private boolean updateCollectionDates() { */ private void updateChannels(final Map collectionDates) { for (final Channel channel : this.getThing().getChannels()) { + final WasteType wasteType = getWasteTypeByChannel(channel.getUID().getId()); final CollectionDate collectionDate = collectionDates.get(wasteType); if (collectionDate == null) { - this.logger.warn("No collection dates found for waste type: {}", wasteType); + this.logger.debug("No collection dates found for waste type: {}", wasteType); continue; } diff --git a/bundles/org.openhab.binding.airquality/README.md b/bundles/org.openhab.binding.airquality/README.md index 156e7e8c9a66a..0a21a20f445f4 100644 --- a/bundles/org.openhab.binding.airquality/README.md +++ b/bundles/org.openhab.binding.airquality/README.md @@ -122,7 +122,7 @@ DateTime Aqi_ObservationTime "Time of observation [%1$tH:%1$tM]" (AirQua Number:Temperature Aqi_Temperature "Temperature" (AirQuality) { channel="airquality:aqi:home:temperature" } Number:Pressure Aqi_Pressure "Pressure" (AirQuality) { channel="airquality:aqi:home:pressure" } -Number:DimensionLess Aqi_Humidity "Humidity" (AirQuality) { channel="airquality:aqi:home:humidity" } +Number:Dimensionless Aqi_Humidity "Humidity" (AirQuality) { channel="airquality:aqi:home:humidity" } ``` airquality.sitemap: diff --git a/bundles/org.openhab.binding.avmfritz/README.md b/bundles/org.openhab.binding.avmfritz/README.md index 9e94b9a9e9b5d..ad2d4fc2d94de 100644 --- a/bundles/org.openhab.binding.avmfritz/README.md +++ b/bundles/org.openhab.binding.avmfritz/README.md @@ -84,6 +84,8 @@ The following sensors have been successfully tested using FRITZ!OS 7 for FRITZ!B - [SmartHome Bewegungsmelder](https://www.smarthome.de/geraete/telekom-smarthome-bewegungsmelder-innen) - a motion sensor (thing type `HAN_FUN_CONTACT`) - [SmartHome Rauchmelder](https://www.smarthome.de/geraete/smarthome-rauchmelder-weiss) - a smoke detector (thing type `HAN_FUN_CONTACT`) - [SmartHome Wandtaster](https://www.smarthome.de/geraete/telekom-smarthome-wandtaster) - a switch with two buttons (thing type `HAN_FUN_SWITCH`) +- [SmartHome Zwischenstecker innen](https://www.smarthome.de/geraete/smarthome-zwischenstecker-innen-weiss) - a switchable indoor outlet (thing type `HAN_FUN_ON_OFF`) +- [SmartHome Zwischenstecker außen](https://www.smarthome.de/geraete/smarthome-zwischenstecker-aussen-schwarz) - a switchable outdoor outlet (thing type `HAN_FUN_ON_OFF`) - [Rollotron DECT 1213](https://www.rademacher.de/shop/rollladen-sonnenschutz/elektrischer-gurtwickler/rollotron-dect-1213) - an electronic belt winder (thing type `HAN_FUN_BLINDS`) - [Becker BoxCTRL](https://becker-antriebe.shop/) - a radio controlled roller shutter drive (thing type `HAN_FUN_BLINDS`) @@ -176,6 +178,7 @@ The AIN (actor identification number) can be found in the FRITZ!Box interface -> | power | Number:Power | Current power consumption | FRITZ!DECT 210, FRITZ!DECT 200, FRITZ!Powerline 546E | | voltage | Number:ElectricPotential | Current voltage - FRITZ!OS 7 | FRITZ!DECT 210, FRITZ!DECT 200, FRITZ!Powerline 546E | | outlet | Switch | Switchable outlet (ON/OFF) | FRITZ!DECT 210, FRITZ!DECT 200, FRITZ!Powerline 546E | +| on_off | Switch | Switchable device (ON/OFF) | HAN_FUN_ON_OFF | | actual_temp | Number:Temperature | Current temperature of heating thermostat | FRITZ!DECT 301, FRITZ!DECT 300, Comet DECT | | set_temp | Number:Temperature | Set Temperature of heating thermostat | FRITZ!DECT 301, FRITZ!DECT 300, Comet DECT | | eco_temp | Number:Temperature | Eco Temperature of heating thermostat | FRITZ!DECT 301, FRITZ!DECT 300, Comet DECT | diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/AVMFritzBindingConstants.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/AVMFritzBindingConstants.java index 792e7e466573e..bbdf5dc3d283a 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/AVMFritzBindingConstants.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/AVMFritzBindingConstants.java @@ -53,6 +53,7 @@ public class AVMFritzBindingConstants { public static final String DEVICE_COMETDECT = "Comet_DECT"; public static final String DEVICE_HAN_FUN_CONTACT = "HAN_FUN_CONTACT"; public static final String DEVICE_HAN_FUN_SWITCH = "HAN_FUN_SWITCH"; + public static final String DEVICE_HAN_FUN_ON_OFF = "HAN_FUN_ON_OFF"; public static final String DEVICE_HAN_FUN_BLINDS = "HAN_FUN_BLINDS"; // List of main group types @@ -74,6 +75,7 @@ public class AVMFritzBindingConstants { public static final ThingTypeUID COMETDECT_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_COMETDECT); public static final ThingTypeUID HAN_FUN_CONTACT_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_HAN_FUN_CONTACT); public static final ThingTypeUID HAN_FUN_SWITCH_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_HAN_FUN_SWITCH); + public static final ThingTypeUID HAN_FUN_ON_OFF_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_HAN_FUN_ON_OFF); public static final ThingTypeUID HAN_FUN_BLINDS_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_HAN_FUN_BLINDS); public static final ThingTypeUID GROUP_HEATING_THING_TYPE = new ThingTypeUID(BINDING_ID, GROUP_HEATING); public static final ThingTypeUID GROUP_SWITCH_THING_TYPE = new ThingTypeUID(BINDING_ID, GROUP_SWITCH); @@ -129,6 +131,7 @@ public class AVMFritzBindingConstants { public static final String CHANNEL_PRESS = "press"; public static final String CHANNEL_LAST_CHANGE = "last_change"; public static final String CHANNEL_ROLLERSHUTTER = "rollershutter"; + public static final String CHANNEL_ON_OFF = "on_off"; // List of all Channel config ids public static final String CONFIG_CHANNEL_TEMP_OFFSET = "offset"; @@ -169,7 +172,7 @@ public class AVMFritzBindingConstants { public static final Set SUPPORTED_DEVICE_THING_TYPES_UIDS = Set.of(DECT100_THING_TYPE, DECT200_THING_TYPE, DECT210_THING_TYPE, PL546E_THING_TYPE, HAN_FUN_CONTACT_THING_TYPE, - HAN_FUN_BLINDS_THING_TYPE); + HAN_FUN_ON_OFF_THING_TYPE, HAN_FUN_BLINDS_THING_TYPE); public static final Set SUPPORTED_GROUP_THING_TYPES_UIDS = Set.of(GROUP_HEATING_THING_TYPE, GROUP_SWITCH_THING_TYPE); diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzBaseModel.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzBaseModel.java index 60e38a1b205dd..82bdae0d630a8 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzBaseModel.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzBaseModel.java @@ -17,6 +17,8 @@ import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; +import org.eclipse.jdt.annotation.Nullable; + /** * See {@link DeviceListModel}. * @@ -34,6 +36,8 @@ *
  • Bit 10: AVM DECT Repeater
  • *
  • Bit 11: Mikrofon
  • *
  • Bit 13: HAN-FUN Unit
  • + *
  • Bit 15: an-/ausschaltbares Gerät / Steckdose / Lampe / Aktor
  • + *
  • Bit 18: Rollladen - hoch, runter, stop und level 0% bis 100 %
  • * * * @author Robert Bausdorf - Initial contribution @@ -53,6 +57,7 @@ public abstract class AVMFritzBaseModel implements BatteryModel { protected static final int DECT_REPEATER_BIT = 1 << 10; // Bit 10 protected static final int MICROPHONE_BIT = 1 << 11; // Bit 11 protected static final int HAN_FUN_UNIT_BIT = 1 << 13; // Bit 13 + protected static final int HAN_FUN_ON_OFF_BIT = 1 << 15; // Bit 15 protected static final int HAN_FUN_BLINDS_BIT = 1 << 18; // Bit 18 protected static final int HUMIDITY_SENSOR_BIT = 1 << 20; // Bit 20 - undocumented @@ -89,12 +94,19 @@ public abstract class AVMFritzBaseModel implements BatteryModel { @XmlElement(name = "switch") private SwitchModel switchModel; + @XmlElement(name = "simpleonoff") + private @Nullable SimpleOnOffModel simpleOnOffUnit; + @XmlElement(name = "powermeter") private PowerMeterModel powermeterModel; @XmlElement(name = "hkr") private HeatingModel heatingModel; + public @Nullable SimpleOnOffModel getSimpleOnOffUnit() { + return simpleOnOffUnit; + } + public PowerMeterModel getPowermeter() { return powermeterModel; } @@ -151,7 +163,7 @@ public boolean isSwitchableOutlet() { return (bitmask & OUTLET_BIT) > 0; } - public boolean isTempSensor() { + public boolean isTemperatureSensor() { return (bitmask & TEMPERATURE_SENSOR_BIT) > 0; } @@ -171,7 +183,7 @@ public boolean isHeatingThermostat() { return (bitmask & HEATING_THERMOSTAT_BIT) > 0; } - public boolean isMicrophone() { + public boolean hasMicrophone() { return (bitmask & MICROPHONE_BIT) > 0; } @@ -179,6 +191,10 @@ public boolean isHANFUNUnit() { return (bitmask & HAN_FUN_UNIT_BIT) > 0; } + public boolean isHANFUNOnOff() { + return (bitmask / HAN_FUN_ON_OFF_BIT) > 0; + } + public boolean isHANFUNBlinds() { return (bitmask & HAN_FUN_BLINDS_BIT) > 0; } @@ -215,19 +231,19 @@ public BigDecimal getBatterylow() { @Override public String toString() { - return new StringBuilder().append("[ain=").append(ident).append(",bitmask=").append(bitmask) - .append(",isHANFUNDevice=").append(isHANFUNDevice()).append(",isHANFUNButton=").append(isHANFUNButton()) + return new StringBuilder("[ain=").append(ident).append(",bitmask=").append(bitmask).append(",isHANFUNDevice=") + .append(isHANFUNDevice()).append(",isHANFUNButton=").append(isHANFUNButton()) .append(",isHANFUNAlarmSensor=").append(isHANFUNAlarmSensor()).append(",isButton=").append(isButton()) - .append(",isSwitchableOutlet=").append(isSwitchableOutlet()).append(",isTempSensor=") - .append(isTempSensor()).append(",isHumiditySensor=").append(isHumiditySensor()).append(",isPowermeter=") - .append(isPowermeter()).append(",isDectRepeater=").append(isDectRepeater()) - .append(",isHeatingThermostat=").append(isHeatingThermostat()).append(",isMicrophone=") - .append(isMicrophone()).append(",isHANFUNUnit=").append(isHANFUNUnit()).append(",isHANFUNBlind=") - .append(isHANFUNBlinds()).append(",id=").append(deviceId).append(",manufacturer=") - .append(deviceManufacturer).append(",productname=").append(productName).append(",fwversion=") - .append(firmwareVersion).append(",present=").append(present).append(",name=").append(name) - .append(",battery=").append(getBattery()).append(",batterylow=").append(getBatterylow()).append(",") - .append(getSwitch()).append(",").append(getPowermeter()).append(",").append(getHkr()).append(",") - .toString(); + .append(",isSwitchableOutlet=").append(isSwitchableOutlet()).append(",isTemperatureSensor=") + .append(isTemperatureSensor()).append(",isHumiditySensor=").append(isHumiditySensor()) + .append(",isPowermeter=").append(isPowermeter()).append(",isDectRepeater=").append(isDectRepeater()) + .append(",isHeatingThermostat=").append(isHeatingThermostat()).append(",hasMicrophone=") + .append(hasMicrophone()).append(",isHANFUNUnit=").append(isHANFUNUnit()).append(",isHANFUNOnOff=") + .append(isHANFUNOnOff()).append(",isHANFUNBlind=").append(isHANFUNBlinds()).append(",id=") + .append(deviceId).append(",manufacturer=").append(deviceManufacturer).append(",productname=") + .append(productName).append(",fwversion=").append(firmwareVersion).append(",present=").append(present) + .append(",name=").append(name).append(",battery=").append(getBattery()).append(",batterylow=") + .append(getBatterylow()).append(",").append(getSwitch()).append(",").append(getSimpleOnOffUnit()) + .append(",").append(getPowermeter()).append(",").append(getHkr()).append(",").toString(); } } diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/DeviceModel.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/DeviceModel.java index 9bfe1bda0e13c..7596026529d83 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/DeviceModel.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/DeviceModel.java @@ -90,25 +90,30 @@ public void setEtsiunitinfo(ETSUnitInfoModel etsiunitinfo) { @Override public String toString() { - return new StringBuilder().append(super.toString()).append(temperature).append(",").append(humidity).append(",") + return new StringBuilder(super.toString()).append(temperature).append(",").append(humidity).append(",") .append(alert).append(",").append(getButtons()).append(",").append(etsiunitinfo).append("]").toString(); } @XmlAccessorType(XmlAccessType.FIELD) @XmlType(propOrder = { "etsideviceid", "unittype", "interfaces" }) public static class ETSUnitInfoModel { + public static final String HAN_FUN_UNITTYPE_AC_OUTLET = "262"; + public static final String HAN_FUN_UNITTYPE_AC_OUTLET_SIMPLE_POWER_METERING = "263"; public static final String HAN_FUN_UNITTYPE_SIMPLE_BUTTON = "273"; public static final String HAN_FUN_UNITTYPE_SIMPLE_DETECTOR = "512"; - public static final String HAN_FUN_UNITTYPE_MAGNETIC_CONTACT = "513"; - public static final String HAN_FUN_UNITTYPE_OPTICAL_CONTACT = "514"; + public static final String HAN_FUN_UNITTYPE_DOOR_OPEN_CLOSE_DETECTOR = "513"; + public static final String HAN_FUN_UNITTYPE_WINDOW_OPEN_CLOSE_DETECTOR = "514"; public static final String HAN_FUN_UNITTYPE_MOTION_DETECTOR = "515"; - public static final String HAN_FUN_UNITTYPE_SMOKE_DETECTOR = "516"; + public static final String HAN_FUN_UNITTYPE_SMOKE_DETECTOR = "516"; // undocumented public static final String HAN_FUN_UNITTYPE_FLOOD_DETECTOR = "518"; public static final String HAN_FUN_UNITTYPE_GLAS_BREAK_DETECTOR = "519"; public static final String HAN_FUN_UNITTYPE_VIBRATION_DETECTOR = "520"; + public static final String HAN_FUN_UNITTYPE_SIREN = "640"; public static final String HAN_FUN_INTERFACE_ALERT = "256"; public static final String HAN_FUN_INTERFACE_KEEP_ALIVE = "277"; + public static final String HAN_FUN_INTERFACE_ON_OFF = "512"; + public static final String HAN_FUN_INTERFACE_SIMPLE_POWER_METERING = "768"; // undocumented public static final String HAN_FUN_INTERFACE_SIMPLE_BUTTON = "772"; private String etsideviceid; @@ -141,8 +146,8 @@ public void setInterfaces(String interfaces) { @Override public String toString() { - return new StringBuilder().append("[etsideviceid=").append(etsideviceid).append(",unittype=") - .append(unittype).append(",interfaces=").append(interfaces).append("]").toString(); + return new StringBuilder("[etsideviceid=").append(etsideviceid).append(",unittype=").append(unittype) + .append(",interfaces=").append(interfaces).append("]").toString(); } } } diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/SimpleOnOffModel.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/SimpleOnOffModel.java new file mode 100644 index 0000000000000..b51ef8b589aec --- /dev/null +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/SimpleOnOffModel.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 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.avmfritz.internal.dto; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + +/** + * See {@link DeviceListModel}. + * + * @author Joshua Bacher - Initial contribution + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(propOrder = { "state" }) +public class SimpleOnOffModel { + + public boolean state; + + @Override + public String toString() { + return new StringBuilder("[state=").append(state).append(']').toString(); + } +} diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseBridgeHandler.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseBridgeHandler.java index 62b3c5180142f..4656fa79d846b 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseBridgeHandler.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseBridgeHandler.java @@ -336,6 +336,8 @@ public String getThingTypeId(AVMFritzBaseModel device) { return DEVICE_HAN_FUN_CONTACT; } else if (interfaces.contains(HAN_FUN_INTERFACE_SIMPLE_BUTTON)) { return DEVICE_HAN_FUN_SWITCH; + } else if (interfaces.contains(HAN_FUN_INTERFACE_ON_OFF)) { + return DEVICE_HAN_FUN_ON_OFF; } } return device.getProductName().replaceAll(INVALID_PATTERN, "_"); diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseThingHandler.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseThingHandler.java index b6759a1c454ee..00debd0235e51 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseThingHandler.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseThingHandler.java @@ -35,6 +35,7 @@ import org.openhab.binding.avmfritz.internal.dto.HumidityModel; import org.openhab.binding.avmfritz.internal.dto.LevelcontrolModel; import org.openhab.binding.avmfritz.internal.dto.PowerMeterModel; +import org.openhab.binding.avmfritz.internal.dto.SimpleOnOffModel; import org.openhab.binding.avmfritz.internal.dto.SwitchModel; import org.openhab.binding.avmfritz.internal.dto.TemperatureModel; import org.openhab.binding.avmfritz.internal.hardware.FritzAhaStatusListener; @@ -140,9 +141,12 @@ public void onDeviceUpdated(ThingUID thingUID, AVMFritzBaseModel device) { if (device.isHeatingThermostat()) { updateHeatingThermostat(device.getHkr()); } + if (device.isHANFUNUnit() && device.isHANFUNOnOff()) { + updateSimpleOnOffUnit(device.getSimpleOnOffUnit()); + } if (device instanceof DeviceModel) { DeviceModel deviceModel = (DeviceModel) device; - if (deviceModel.isTempSensor()) { + if (deviceModel.isTemperatureSensor()) { updateTemperatureSensor(deviceModel.getTemperature()); } if (deviceModel.isHumiditySensor()) { @@ -225,8 +229,13 @@ protected void updateBattery(BatteryModel batteryModel) { if (lowBattery == null) { updateThingChannelState(CHANNEL_BATTERY_LOW, UnDefType.UNDEF); } else { - updateThingChannelState(CHANNEL_BATTERY_LOW, - BatteryModel.BATTERY_ON.equals(lowBattery) ? OnOffType.ON : OnOffType.OFF); + updateThingChannelState(CHANNEL_BATTERY_LOW, OnOffType.from(BatteryModel.BATTERY_ON.equals(lowBattery))); + } + } + + private void updateSimpleOnOffUnit(@Nullable SimpleOnOffModel simpleOnOffUnit) { + if (simpleOnOffUnit != null) { + updateThingChannelState(CHANNEL_ON_OFF, OnOffType.from(simpleOnOffUnit.state)); } } @@ -241,7 +250,7 @@ private void updateSwitchableOutlet(@Nullable SwitchModel switchModel) { if (state == null) { updateThingChannelState(CHANNEL_OUTLET, UnDefType.UNDEF); } else { - updateThingChannelState(CHANNEL_OUTLET, SwitchModel.ON.equals(state) ? OnOffType.ON : OnOffType.OFF); + updateThingChannelState(CHANNEL_OUTLET, OnOffType.from(SwitchModel.ON.equals(state))); } } } @@ -370,11 +379,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command); break; case CHANNEL_OUTLET: + case CHANNEL_ON_OFF: if (command instanceof OnOffType) { fritzBox.setSwitch(ain, OnOffType.ON.equals(command)); - if (state != null) { - state.getSwitch().setState(OnOffType.ON.equals(command) ? SwitchModel.ON : SwitchModel.OFF); - } } break; case CHANNEL_SETTEMP: diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/Powerline546EHandler.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/Powerline546EHandler.java index b31128851c31e..dc5176a0c4788 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/Powerline546EHandler.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/Powerline546EHandler.java @@ -66,10 +66,6 @@ public class Powerline546EHandler extends AVMFritzBaseBridgeHandler implements F private final Logger logger = LoggerFactory.getLogger(Powerline546EHandler.class); - /** - * keeps track of the current state for handling of increase/decrease - */ - private @Nullable AVMFritzBaseModel state; private @Nullable String identifier; /** @@ -128,7 +124,6 @@ public void onDeviceUpdated(ThingUID thingUID, AVMFritzBaseModel device) { } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device not present"); } - state = device; updateProperties(device); @@ -272,11 +267,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } break; case CHANNEL_OUTLET: - fritzBox.setSwitch(ain, OnOffType.ON.equals(command)); if (command instanceof OnOffType) { - if (state != null) { - state.getSwitch().setState(OnOffType.ON.equals(command) ? SwitchModel.ON : SwitchModel.OFF); - } + fritzBox.setSwitch(ain, OnOffType.ON.equals(command)); } break; default: diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/callbacks/FritzAhaUpdateCallback.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/callbacks/FritzAhaUpdateCallback.java index 31caca0941431..c98c35802adfe 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/callbacks/FritzAhaUpdateCallback.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/callbacks/FritzAhaUpdateCallback.java @@ -17,6 +17,7 @@ import java.io.StringReader; import javax.xml.bind.JAXBException; +import javax.xml.bind.UnmarshalException; import javax.xml.bind.Unmarshaller; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; @@ -58,6 +59,7 @@ public FritzAhaUpdateCallback(FritzAhaWebInterface webIface, AVMFritzBaseBridgeH this.handler = handler; } + @SuppressWarnings({ "null", "unused" }) @Override public void execute(int status, String response) { super.execute(status, response); @@ -66,13 +68,16 @@ public void execute(int status, String response) { try { XMLStreamReader xsr = JAXBUtils.XMLINPUTFACTORY.createXMLStreamReader(new StringReader(response)); Unmarshaller unmarshaller = JAXBUtils.JAXBCONTEXT_DEVICES.createUnmarshaller(); - DeviceListModel model = (DeviceListModel) unmarshaller.unmarshal(xsr); + DeviceListModel model = unmarshaller.unmarshal(xsr, DeviceListModel.class).getValue(); if (model != null) { handler.onDeviceListAdded(model.getDevicelist()); } else { logger.debug("no model in response"); } handler.setStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); + } catch (UnmarshalException e) { + logger.debug("Failed to unmarshal XML document: {}", e.getMessage()); + handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } catch (JAXBException | XMLStreamException e) { logger.error("Exception creating Unmarshaller: {}", e.getLocalizedMessage(), e); handler.setStatusInfo(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/callbacks/FritzAhaUpdateTemplatesCallback.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/callbacks/FritzAhaUpdateTemplatesCallback.java index d976c3b35e014..702b2973702cd 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/callbacks/FritzAhaUpdateTemplatesCallback.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/hardware/callbacks/FritzAhaUpdateTemplatesCallback.java @@ -17,6 +17,7 @@ import java.io.StringReader; import javax.xml.bind.JAXBException; +import javax.xml.bind.UnmarshalException; import javax.xml.bind.Unmarshaller; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; @@ -54,6 +55,7 @@ public FritzAhaUpdateTemplatesCallback(FritzAhaWebInterface webInterface, AVMFri this.handler = handler; } + @SuppressWarnings({ "null", "unused" }) @Override public void execute(int status, String response) { super.execute(status, response); @@ -62,12 +64,14 @@ public void execute(int status, String response) { try { XMLStreamReader xsr = JAXBUtils.XMLINPUTFACTORY.createXMLStreamReader(new StringReader(response)); Unmarshaller unmarshaller = JAXBUtils.JAXBCONTEXT_TEMPLATES.createUnmarshaller(); - TemplateListModel model = (TemplateListModel) unmarshaller.unmarshal(xsr); + TemplateListModel model = unmarshaller.unmarshal(xsr, TemplateListModel.class).getValue(); if (model != null) { handler.addTemplateList(model.getTemplates()); } else { logger.debug("no template in response"); } + } catch (UnmarshalException e) { + logger.debug("Failed to unmarshal XML document: {}", e.getMessage()); } catch (JAXBException | XMLStreamException e) { logger.error("Exception creating Unmarshaller: {}", e.getLocalizedMessage(), e); } diff --git a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/i18n/avmfritz_de.properties b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/i18n/avmfritz_de.properties index b0ebf7f0924dc..f871defc66982 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/i18n/avmfritz_de.properties +++ b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/i18n/avmfritz_de.properties @@ -13,6 +13,12 @@ thing-type.avmfritz.HAN_FUN_CONTACT.description = HAN-FUN Kontakt (e.g. SmartHom thing-type.avmfritz.HAN_FUN_SWITCH.label = HAN-FUN Schalter thing-type.avmfritz.HAN_FUN_SWITCH.description = HAN-FUN Schalter (e.g. SmartHome Wandtaster). +thing-type.avmfritz.HAN_FUN_BLINDS.label = HAN-FUN Rollladen +thing-type.avmfritz.HAN_FUN_BLINDS.description = HAN-FUN Rollladen (z.B. Rollotron DECT 1213, Becker BoxCTRL). + +thing-type.avmfritz.HAN_FUN_ON_OFF.label = HAN-FUN an-/ausschaltbares Gert +thing-type.avmfritz.HAN_FUN_ON_OFF.description = HAN-FUN an-/ausschaltbares Gert (e.g. SmartHome Zwischenstecker innen / SmartHome Zwischenstecker auen). + # bridge types config groups bridge-type.config.avmfritz.fritzbox.group.network.label = Netzwerk bridge-type.config.avmfritz.fritzbox.group.network.description = Einstellungen fr das Netzwerk. @@ -99,9 +105,6 @@ thing-type.avmfritz.FRITZ_DECT_440.description = FRITZ!DECT 440 Taster. Dient zu thing-type.avmfritz.FRITZ_Powerline_546E.description = FRITZ!Powerline 546E schaltbare Steckdose. Dient zur Steuerung der integrierten Steckdose und liefert Daten wie z.B. Temperatur. -thing-type.avmfritz.HAN_FUN_BLINDS.label = HAN-FUN Rollladen -thing-type.avmfritz.HAN_FUN_BLINDS.description = HAN-FUN Rollladen (z.B. Rollotron DECT 1213, Becker BoxCTRL). - # thing types config groups thing-type.avmfritz.FRITZ_GROUP_HEATING.label = Heizkrperregler thing-type.avmfritz.FRITZ_GROUP_HEATING.description = Gruppe fr Heizkrperregler. Dient zur Steuerung von Heizkrpern und liefert Daten wie z.B. Temperatur. diff --git a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/thing-types.xml index bdf606ed242c1..c4f11d5ab8f5b 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/thing-types.xml @@ -295,6 +295,24 @@ + + + + + + + + HAN-FUN switchable device (e.g. SmartHome Zwischenstecker innen / SmartHome Zwischenstecker außen) + + + + + + ain + + + + diff --git a/bundles/org.openhab.binding.avmfritz/src/test/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzDeviceListModelTest.java b/bundles/org.openhab.binding.avmfritz/src/test/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzDeviceListModelTest.java index 3606d52ef6a0f..32fd63ab7d652 100644 --- a/bundles/org.openhab.binding.avmfritz/src/test/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzDeviceListModelTest.java +++ b/bundles/org.openhab.binding.avmfritz/src/test/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzDeviceListModelTest.java @@ -21,13 +21,13 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.openhab.binding.avmfritz.internal.util.JAXBUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Tests for {@link DeviceListModel}. @@ -38,43 +38,65 @@ @NonNullByDefault public class AVMFritzDeviceListModelTest { - private final Logger logger = LoggerFactory.getLogger(AVMFritzDeviceListModelTest.class); - private @NonNullByDefault({}) DeviceListModel devices; + @SuppressWarnings("null") @BeforeEach - public void setUp() { + public void setUp() throws JAXBException, XMLStreamException { //@formatter:off final String xml = - "" + - "1Schlafzimmer1manuell00230051020871717,18" + - "1Schlafzimmer220-104442284211000000100148434120028020,21,22" + - "1FRITZ!DECT 200 #11manuell00230051020872550" + - "1FRITZ!DECT 210 #81manuell00230051020872550" + - "0FRITZ!DECT 300 #1220-104442284211000000100148434120028" + - "0FRITZ!DECT 301 #1220-104442284211000000100148434120028" + - "0Comet DECT #1220-104442284211000000100148434120028" + - "1FRITZ!Powerline 546E #10manuell0123005102087" + - "1FRITZ!DECT Repeater 100 #52300" + - "0HAN-FUN #2: Unit #24065142561" + - "0HAN-FUN #2: Unit #2412273772" + - "1FRITZ!DECT 400 #141000" + - "1FRITZ!DECT 440 #152300431000" + - "10Rollotron 1213 #11manuell2610406281256,513,516,5170" + + "" + + "1Schlafzimmer1manuell00230051020871717,18" + + "1Schlafzimmer220-104442284211000000100148434120028020,21,22" + + "1FRITZ!DECT 200 #11manuell00230051020872550" + + "1FRITZ!DECT 210 #81manuell00230051020872550" + + "0FRITZ!DECT 300 #1220-104442284211000000100148434120028" + + "0FRITZ!DECT 301 #1220-104442284211000000100148434120028" + + "0Comet DECT #1220-104442284211000000100148434120028" + + "1FRITZ!Powerline 546E #10manuell0123005102087" + + "1FRITZ!DECT Repeater 100 #52300" + + "0HAN-FUN #2: Unit #24065142561" + + "0HAN-FUN #2: Unit #2412273772" + + "1FRITZ!DECT 400 #141000" + + "1FRITZ!DECT 440 #152300431000" + + "10Rollotron 1213 #11manuell2610406281256,513,516,5170" + + "\n" + + " 1\n" + + " 0\n" + + " Steckdose innen\n" + + " \n" + + " 0\n" + + " \n" + + " \n" + + " 408\n" + + " 263\n" + + " 512,768\n" + + " \n" + + "" + + "\n" + + " 1\n" + + " 0\n" + + " Steckdose außen\n" + + " \n" + + " 0\n" + + " \n" + + " \n" + + " 407\n" + + " 262\n" + + " 512\n" + + " \n" + + "" + ""; - //@formatter:off - try { - Unmarshaller u = JAXBUtils.JAXBCONTEXT_DEVICES.createUnmarshaller(); - devices = (DeviceListModel) u.unmarshal(new StringReader(xml)); - } catch (JAXBException e) { - logger.error("Exception creating Unmarshaller: {}", e.getLocalizedMessage(), e); - } + //@formatter:on + XMLStreamReader xsr = JAXBUtils.XMLINPUTFACTORY.createXMLStreamReader(new StringReader(xml)); + Unmarshaller u = JAXBUtils.JAXBCONTEXT_DEVICES.createUnmarshaller(); + devices = u.unmarshal(xsr, DeviceListModel.class).getValue(); } @Test public void validateDeviceListModel() { assertNotNull(devices); - assertEquals(14, devices.getDevicelist().size()); + assertEquals(16, devices.getDevicelist().size()); assertEquals("1", devices.getXmlApiVersion()); } @@ -99,7 +121,7 @@ public void validateDECTRepeater100Model() { assertFalse(device.isHANFUNAlarmSensor()); assertTrue(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertTrue(device.isTempSensor()); + assertTrue(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertFalse(device.isHeatingThermostat()); @@ -134,16 +156,19 @@ public void validateDECT200Model() { assertEquals(1, device.getPresent()); assertEquals("FRITZ!DECT 200 #1", device.getName()); - assertFalse(device.isButton()); assertFalse(device.isHANFUNButton()); assertFalse(device.isHANFUNAlarmSensor()); - assertFalse(device.isDectRepeater()); - assertTrue(device.isSwitchableOutlet()); - assertTrue(device.isTempSensor()); - assertFalse(device.isHumiditySensor()); - assertTrue(device.isPowermeter()); + assertFalse(device.isButton()); assertFalse(device.isHeatingThermostat()); + assertTrue(device.isPowermeter()); + assertTrue(device.isTemperatureSensor()); + assertTrue(device.isSwitchableOutlet()); + assertFalse(device.isDectRepeater()); + assertTrue(device.hasMicrophone()); + assertFalse(device.isHANFUNUnit()); + assertTrue(device.isHANFUNOnOff()); assertFalse(device.isHANFUNBlinds()); + assertFalse(device.isHumiditySensor()); assertNotNull(device.getSwitch()); assertEquals(SwitchModel.ON, device.getSwitch().getState()); @@ -178,16 +203,19 @@ public void validateDECT210Model() { assertEquals(1, device.getPresent()); assertEquals("FRITZ!DECT 210 #8", device.getName()); - assertFalse(device.isButton()); assertFalse(device.isHANFUNButton()); assertFalse(device.isHANFUNAlarmSensor()); - assertFalse(device.isDectRepeater()); - assertTrue(device.isSwitchableOutlet()); - assertTrue(device.isTempSensor()); - assertFalse(device.isHumiditySensor()); - assertTrue(device.isPowermeter()); + assertFalse(device.isButton()); assertFalse(device.isHeatingThermostat()); + assertTrue(device.isPowermeter()); + assertTrue(device.isTemperatureSensor()); + assertTrue(device.isSwitchableOutlet()); + assertFalse(device.isDectRepeater()); + assertTrue(device.hasMicrophone()); + assertFalse(device.isHANFUNUnit()); + assertTrue(device.isHANFUNOnOff()); assertFalse(device.isHANFUNBlinds()); + assertFalse(device.isHumiditySensor()); assertNotNull(device.getSwitch()); assertEquals(SwitchModel.ON, device.getSwitch().getState()); @@ -227,7 +255,7 @@ public void validateDECT300Model() { assertFalse(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertTrue(device.isTempSensor()); + assertTrue(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertTrue(device.isHeatingThermostat()); @@ -265,7 +293,7 @@ public void validateDECT301Model() { assertFalse(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertTrue(device.isTempSensor()); + assertTrue(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertTrue(device.isHeatingThermostat()); @@ -303,7 +331,7 @@ public void validateCometDECTModel() { assertFalse(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertTrue(device.isTempSensor()); + assertTrue(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertTrue(device.isHeatingThermostat()); @@ -341,7 +369,7 @@ public void validateDECT400Model() { assertFalse(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertFalse(device.isTempSensor()); + assertFalse(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertFalse(device.isHeatingThermostat()); @@ -390,7 +418,7 @@ public void validateDECT440Model() { assertFalse(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertTrue(device.isTempSensor()); + assertTrue(device.isTemperatureSensor()); assertTrue(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertFalse(device.isHeatingThermostat()); @@ -460,7 +488,7 @@ public void validatePowerline546EModel() { assertFalse(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertTrue(device.isSwitchableOutlet()); - assertFalse(device.isTempSensor()); + assertFalse(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertTrue(device.isPowermeter()); assertFalse(device.isHeatingThermostat()); @@ -502,7 +530,7 @@ public void validateHANFUNContactModel() { assertTrue(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertFalse(device.isTempSensor()); + assertFalse(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertFalse(device.isHeatingThermostat()); @@ -545,7 +573,7 @@ public void validateHANFUNSwitchModel() { assertFalse(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertFalse(device.isTempSensor()); + assertFalse(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertFalse(device.isHeatingThermostat()); @@ -588,7 +616,7 @@ public void validateHANFUNBlindModel() { assertTrue(device.isHANFUNAlarmSensor()); assertFalse(device.isDectRepeater()); assertFalse(device.isSwitchableOutlet()); - assertFalse(device.isTempSensor()); + assertFalse(device.isTemperatureSensor()); assertFalse(device.isHumiditySensor()); assertFalse(device.isPowermeter()); assertFalse(device.isHeatingThermostat()); @@ -613,6 +641,55 @@ public void validateHANFUNBlindModel() { assertEquals(BigDecimal.valueOf(10L), levelcontrol.getLevelPercentage()); } + @Test + public void validateHANFUNOnOffModel() { + Optional optionalDevice = findModelByIdentifier("113240824499-1"); + assertTrue(optionalDevice.isPresent()); + assertTrue(optionalDevice.get() instanceof DeviceModel); + + DeviceModel device = (DeviceModel) optionalDevice.get(); + assertEquals("HAN-FUN", device.getProductName()); + assertEquals("113240824499-1", device.getIdentifier()); + assertEquals("2002", device.getDeviceId()); + assertEquals("0.0", device.getFirmwareVersion()); + assertEquals("0x2c3c", device.getManufacturer()); + + assertEquals(1, device.getPresent()); + assertEquals("Steckdose innen", device.getName()); + + assertFalse(device.isHANFUNButton()); + assertFalse(device.isHANFUNAlarmSensor()); + assertFalse(device.isButton()); + assertFalse(device.isHeatingThermostat()); + assertFalse(device.isPowermeter()); + assertFalse(device.isTemperatureSensor()); + assertFalse(device.isSwitchableOutlet()); + assertFalse(device.isDectRepeater()); + assertFalse(device.hasMicrophone()); + assertTrue(device.isHANFUNUnit()); + assertTrue(device.isHANFUNOnOff()); + assertFalse(device.isHANFUNBlinds()); + assertFalse(device.isHumiditySensor()); + + assertTrue(device.getButtons().isEmpty()); + + assertNull(device.getAlert()); + + assertNull(device.getSwitch()); + + assertNull(device.getTemperature()); + + SimpleOnOffModel model = device.getSimpleOnOffUnit(); + assertNotNull(model); + assertEquals(false, model.state); + + assertNull(device.getPowermeter()); + + assertNull(device.getHkr()); + + assertNull(device.getLevelcontrol()); + } + @Test public void validateHeatingGroupModel() { Optional optionalGroup = findModelByIdentifier("F0:A3:7F-901"); @@ -634,7 +711,7 @@ public void validateHeatingGroupModel() { assertFalse(group.isHANFUNAlarmSensor()); assertFalse(group.isDectRepeater()); assertFalse(group.isSwitchableOutlet()); - assertFalse(group.isTempSensor()); + assertFalse(group.isTemperatureSensor()); assertFalse(group.isHumiditySensor()); assertFalse(group.isPowermeter()); assertTrue(group.isHeatingThermostat()); @@ -672,7 +749,7 @@ public void validateSwitchGroupModel() { assertFalse(group.isHANFUNAlarmSensor()); assertFalse(group.isDectRepeater()); assertTrue(group.isSwitchableOutlet()); - assertFalse(group.isTempSensor()); + assertFalse(group.isTemperatureSensor()); assertFalse(group.isHumiditySensor()); assertTrue(group.isPowermeter()); assertFalse(group.isHeatingThermostat()); diff --git a/bundles/org.openhab.binding.avmfritz/src/test/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzTemplateListModelTest.java b/bundles/org.openhab.binding.avmfritz/src/test/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzTemplateListModelTest.java index 6691c8f91c3aa..3b7c0fc408029 100644 --- a/bundles/org.openhab.binding.avmfritz/src/test/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzTemplateListModelTest.java +++ b/bundles/org.openhab.binding.avmfritz/src/test/java/org/openhab/binding/avmfritz/internal/dto/AVMFritzTemplateListModelTest.java @@ -19,6 +19,8 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; @@ -26,8 +28,6 @@ import org.openhab.binding.avmfritz.internal.dto.templates.TemplateListModel; import org.openhab.binding.avmfritz.internal.dto.templates.TemplateModel; import org.openhab.binding.avmfritz.internal.util.JAXBUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Tests for {@link TemplateListModel}. @@ -37,26 +37,21 @@ @NonNullByDefault public class AVMFritzTemplateListModelTest { - private final Logger logger = LoggerFactory.getLogger(AVMFritzTemplateListModelTest.class); - private @NonNullByDefault({}) TemplateListModel templates; + @SuppressWarnings("null") @BeforeEach - public void setUp() { + public void setUp() throws JAXBException, XMLStreamException { //@formatter:off String xml = "" + "" + "" + ""; - //@formatter:off - - try { - Unmarshaller u = JAXBUtils.JAXBCONTEXT_TEMPLATES.createUnmarshaller(); - templates = (TemplateListModel) u.unmarshal(new StringReader(xml)); - } catch (JAXBException e) { - logger.error("Exception creating Unmarshaller: {}", e.getLocalizedMessage(), e); - } + //@formatter:on + XMLStreamReader xsr = JAXBUtils.XMLINPUTFACTORY.createXMLStreamReader(new StringReader(xml)); + Unmarshaller u = JAXBUtils.JAXBCONTEXT_TEMPLATES.createUnmarshaller(); + templates = u.unmarshal(xsr, TemplateListModel.class).getValue(); } @Test diff --git a/bundles/org.openhab.binding.benqprojector/NOTICE b/bundles/org.openhab.binding.benqprojector/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/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.benqprojector/README.md b/bundles/org.openhab.binding.benqprojector/README.md new file mode 100644 index 0000000000000..c306fb5dff73f --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/README.md @@ -0,0 +1,115 @@ +# BenQ Projector Binding + +This binding is compatible with BenQ projectors that support the control protocol via the built-in ethernet port, serial port or USB to serial adapter. +If your projector does not have built-in networking, you can connect to your projector's serial port via a TCP connection using a serial over IP device or by using`ser2net`. + +The control protocol can be found here: https://business-display.benq.com/content/dam/bb/en/product/projector/corporate/lx770/quick-start-guide/lx770-rs232-control-guide-0-windows7-windows8-winxp.pdf + +## Supported Things + +This binding supports two thing types based on the connection used: `projector-serial` and `projector-tcp`. + +## Discovery + +The projector thing cannot be auto-discovered, it has to be configured manually. + +## Binding Configuration + +There are no overall binding configuration settings that need to be set. +All settings are through thing configuration parameters. + +## Thing Configuration + +The `projector-serial` thing has the following configuration parameters: + +| Parameter | Name | Description | Required | +|-----------------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| +| serialPort | Serial Port | Serial port device name that is connected to the BenQ projector to control, e.g. COM1 on Windows, /dev/ttyS0 on Linux or /dev/tty.PL2303-0000103D on Mac. | yes | +| pollingInterval | Polling Interval | Polling interval in seconds to update channel states, range 5-60 seconds; default 10 seconds. | no | + +The `projector-tcp` thing has the following configuration parameters: + +| Parameter | Name | Description | Required | +|-----------------|------------------|-------------------------------------------------------------------------------------------------------------|----------| +| host | Host Name | Host Name or IP address for the projector or serial over IP device. | yes | +| port | Port | Port for the projector or serial over IP device, Default 8000 for BenQ projectors with built in networking. | yes | +| pollingInterval | Polling Interval | Polling interval in seconds to update channel states, range 5-60 seconds; default 10 seconds. | no | + +Some notes: + +* If using a serial port connection, the baud rate in the projector OSD menu must be set to 9600 bps. +* The _source_, _picturemode_ and _aspectratio_ channels include a dropdown with the most commonly used settings. +* Not all pre-defined dropdown options will be usable if your particular projector does support a given option. +* If your projector has an option that is not in the dropdown, the string code to access that option will be displayed by the channel when that option is selected by the remote control. +* By using the sitemap mapping or a rule to send that code back to the channel, any options that are missing in the binding can be accessed. + +* On Linux, you may get an error stating the serial port cannot be opened when the benqprojector binding tries to load. +* You can get around this by adding the `openhab` user to the `dialout` group like this: `usermod -a -G dialout openhab`. +* Also on Linux you may have issues with the USB if using two serial USB devices e.g. benqprojector and RFXcom. See the [general documentation about serial port configuration](/docs/administration/serial.html) for more on symlinking the USB ports. +* Here is an example of ser2net.conf you can use to share your serial port /dev/ttyUSB0 on IP port 4444 using [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) (take care, the baud rate is specific to the BenQ projector): + +``` +4444:raw:0:/dev/ttyUSB0:9600 8DATABITS NONE 1STOPBIT LOCAL +``` + +## Channels + +| Channel | Item Type | Purpose | Values | +| ------------------ | --------- | --------------------------------------------------- | --------- | +| power | Switch | Powers the projector on or off. | | +| source | String | Retrieve or set the input source. | See above | +| picturemode | String | Retrieve or set the picture mode. | See above | +| aspectratio | String | Retrieve or set the aspect ratio. | See above | +| freeze | Switch | Turn the freeze image mode on or off. | | +| blank | Switch | Turn the screen blank mode on or off. | | +| directcmd | String | Send a command directly to the projector. | Send only | +| lamptime | Number | Retrieves the lamp hours. | Read only | + +## Full Example + +things/benq.things: + +``` +//serial port connection +benqprojector:projector-serial:hometheater "Projector" [ serialPort="COM5", pollingInterval=10 ] + +// serial over IP connection +benqprojector:projector-tcp:hometheater "Projector" [ host="192.168.0.10", port=8000, pollingInterval=10 ] + +``` + +items/benq.items + +``` +Switch benqPower { channel="benqprojector:projector-serial:hometheater:power" } +String benqSource "Source [%s]" { channel="benqprojector:projector-serial:hometheater:source" } +String benqPictureMode "Picture Mode [%s]" { channel="benqprojector:projector-serial:hometheater:picturemode" } +String benqAspectRatio "Aspect Ratio [%s]" { channel="benqprojector:projector-serial:hometheater:aspectratio" } +Switch benqFreeze { channel="benqprojector:projector-serial:hometheater:freeze" } +Switch benqBlank { channel="benqprojector:projector-serial:hometheater:blank" } +String benqDirect { channel="benqprojector:projector-serial:hometheater:directcmd", autoupdate="false" } +Number benqLampTime "Lamp Time [%d h]" { channel="benqprojector:projector-serial:hometheater:lamptime" } +``` + +sitemaps/benq.sitemap + +``` +sitemap benq label="BenQ Projector Demo" { + Frame label="Controls" { + Switch item=benqPower label="Power" + Selection item=benqSource label="Source" mappings=["hdmi"="HDMI", "hdmi2"="HDMI2", "ypbr"="Component", "RGB"="Computer", "vid"="Video", "svid"="S-Video"] + Selection item=benqPictureMode label="Picture Mode" + Selection item=benqAspectRatio label="Aspect Ratio" + Switch item=benqFreeze label="Freeze" + Switch item=benqBlank label="Blank Screen" + Selection item=benqDirect label="Direct Command" + Text item=benqLampTime + } + Frame label="Advanced Controls" { + Switch item=benqDirect label="Image Flip" mappings=["pp=FT"="Front","pp=RE"="Rear","pp=FC"="Front Ceiling","pp=RC"="Rear Ceiling"] + Switch item=benqDirect label="Load Lens Memory" mappings=["lensload=m1"="1","lensload=m2"="2","lensload=m3"="3","lensload=m4"="4"] + Switch item=benqDirect label="Lamp Mode" mappings=["lampm=lnor"="Normal","lampm=eco"="Eco","lampm=seco"="SmartEco"] + Switch item=benqDirect label="Lamp Mode" mappings=["lampm=seco2"="SmartEco2","lampm=seco3"="SmartEco3","lampm=dimming"="Dimming","lampm=custom"="Custom"] + } +} +``` diff --git a/bundles/org.openhab.binding.benqprojector/pom.xml b/bundles/org.openhab.binding.benqprojector/pom.xml new file mode 100644 index 0000000000000..472c773945d5c --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.benqprojector + + openHAB Add-ons :: Bundles :: BenQ Projector Binding + + diff --git a/bundles/org.openhab.binding.benqprojector/src/main/feature/feature.xml b/bundles/org.openhab.binding.benqprojector/src/main/feature/feature.xml new file mode 100644 index 0000000000000..cc4133c31b19b --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-serial + mvn:org.openhab.addons.bundles/org.openhab.binding.benqprojector/${project.version} + + diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorBindingConstants.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorBindingConstants.java new file mode 100644 index 0000000000000..69b0fe4059e8e --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorBindingConstants.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link BenqProjectorBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class BenqProjectorBindingConstants { + + private static final String BINDING_ID = "benqprojector"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_PROJECTOR_SERIAL = new ThingTypeUID(BINDING_ID, "projector-serial"); + public static final ThingTypeUID THING_TYPE_PROJECTOR_TCP = new ThingTypeUID(BINDING_ID, "projector-tcp"); + + // Some Channel types + public static final String CHANNEL_TYPE_POWER = "power"; +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorCommandException.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorCommandException.java new file mode 100644 index 0000000000000..8078b1008ae38 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorCommandException.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception for BenQ projector command errors. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class BenqProjectorCommandException extends Exception { + + private static final long serialVersionUID = -8048415193494625295L; + + public BenqProjectorCommandException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorCommandType.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorCommandType.java new file mode 100644 index 0000000000000..f1a67f4152899 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorCommandType.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal; + +import java.io.InvalidClassException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.Item; +import org.openhab.core.library.items.NumberItem; +import org.openhab.core.library.items.StringItem; +import org.openhab.core.library.items.SwitchItem; + +/** + * Represents all valid command types which could be processed by this + * binding. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public enum BenqProjectorCommandType { + POWER("Power", SwitchItem.class), + SOURCE("Source", StringItem.class), + PICTURE_MODE("PictureMode", StringItem.class), + ASPECT_RATIO("AspectRatio", StringItem.class), + FREEZE("Freeze", SwitchItem.class), + BLANK("Blank", SwitchItem.class), + DIRECTCMD("DirectCmd", StringItem.class), + LAMP_TIME("LampTime", NumberItem.class); + + private final String text; + private Class itemClass; + + private BenqProjectorCommandType(final String text, Class itemClass) { + this.text = text; + this.itemClass = itemClass; + } + + @Override + public String toString() { + return text; + } + + public Class getItemClass() { + return itemClass; + } + + /** + * Procedure to validate command type string. + * + * @param commandTypeText + * command string e.g. RawData, Command, Brightness + * @return true if item is valid. + * @throws IllegalArgumentException + * Not valid command type. + * @throws InvalidClassException + * Not valid class for command type. + */ + public static boolean validateBinding(String commandTypeText, Class itemClass) + throws IllegalArgumentException, InvalidClassException { + for (BenqProjectorCommandType c : BenqProjectorCommandType.values()) { + if (c.text.equalsIgnoreCase(commandTypeText)) { + if (c.getItemClass().equals(itemClass)) { + return true; + } else { + throw new InvalidClassException("Not valid class for command type"); + } + } + } + + throw new IllegalArgumentException("Not valid command type"); + } + + /** + * Procedure to convert command type string to command type class. + * + * @param commandTypeText + * command string e.g. RawData, Command, Brightness + * @return corresponding command type. + * @throws InvalidClassException + * Not valid class for command type. + */ + public static BenqProjectorCommandType getCommandType(String commandTypeText) throws IllegalArgumentException { + for (BenqProjectorCommandType c : BenqProjectorCommandType.values()) { + if (c.text.equalsIgnoreCase(commandTypeText)) { + return c; + } + } + + throw new IllegalArgumentException("Not valid command type"); + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorDevice.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorDevice.java new file mode 100644 index 0000000000000..169ea8c738a34 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorDevice.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.benqprojector.internal.configuration.BenqProjectorConfiguration; +import org.openhab.binding.benqprojector.internal.connector.BenqProjectorConnector; +import org.openhab.binding.benqprojector.internal.connector.BenqProjectorSerialConnector; +import org.openhab.binding.benqprojector.internal.connector.BenqProjectorTcpConnector; +import org.openhab.binding.benqprojector.internal.enums.Switch; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.io.transport.serial.SerialPortManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provide high level interface to BenQ projector. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class BenqProjectorDevice { + private static final String UNSUPPORTED_ITM = "Unsupported item"; + private static final String BLOCK_ITM = "Block item"; + private static final String ILLEGAL_FMT = "Illegal format"; + + private static final int LAMP_REFRESH_WAIT_MINUTES = 5; + + private ExpiringCache cachedLampHours = new ExpiringCache<>(Duration.ofMinutes(LAMP_REFRESH_WAIT_MINUTES), + this::queryLamp); + + private final Logger logger = LoggerFactory.getLogger(BenqProjectorDevice.class); + + private BenqProjectorConnector connection; + private boolean connected = false; + + public BenqProjectorDevice(SerialPortManager serialPortManager, BenqProjectorConfiguration config) { + connection = new BenqProjectorSerialConnector(serialPortManager, config.serialPort); + } + + public BenqProjectorDevice(BenqProjectorConfiguration config) { + connection = new BenqProjectorTcpConnector(config.host, config.port); + } + + private synchronized String sendQuery(String query) throws BenqProjectorCommandException, BenqProjectorException { + logger.debug("Query: '{}'", query); + String response = connection.sendMessage(query); + + if (response.length() == 0) { + throw new BenqProjectorException("No response received"); + } + + if (response.contains(UNSUPPORTED_ITM)) { + return "UNSUPPORTED"; + } + + if (response.contains(BLOCK_ITM)) { + throw new BenqProjectorCommandException("Block Item received for command: " + query); + } + + if (response.contains(ILLEGAL_FMT)) { + throw new BenqProjectorCommandException("Illegal Format response received for command: " + query); + } + + logger.debug("Response: '{}'", response); + + // example: SOUR=HDMI2 + String[] responseParts = response.split("="); + if (responseParts.length != 2) { + throw new BenqProjectorCommandException("Invalid respose for command: " + query); + } + + return responseParts[1].toLowerCase(); + } + + protected void sendCommand(String command) throws BenqProjectorCommandException, BenqProjectorException { + sendQuery(command); + } + + protected int queryInt(String query) throws BenqProjectorCommandException, BenqProjectorException { + String response = sendQuery(query); + return Integer.parseInt(response); + } + + protected String queryString(String query) throws BenqProjectorCommandException, BenqProjectorException { + return sendQuery(query); + } + + public void connect() throws BenqProjectorException { + connection.connect(); + connected = true; + } + + public void disconnect() throws BenqProjectorException { + connection.disconnect(); + connected = false; + } + + public boolean isConnected() { + return connected; + } + + /* + * Power + */ + public Switch getPowerStatus() throws BenqProjectorCommandException, BenqProjectorException { + return (queryString("pow=?").contains("on") ? Switch.ON : Switch.OFF); + } + + public void setPower(Switch value) throws BenqProjectorCommandException, BenqProjectorException { + sendCommand(value == Switch.ON ? "pow=on" : "pow=off"); + } + + /* + * Source + */ + public @Nullable String getSource() throws BenqProjectorCommandException, BenqProjectorException { + return queryString("sour=?"); + } + + public void setSource(String value) throws BenqProjectorCommandException, BenqProjectorException { + sendCommand(String.format("sour=%s", value)); + } + + /* + * Picture Mode + */ + public @Nullable String getPictureMode() throws BenqProjectorCommandException, BenqProjectorException { + return queryString("appmod=?"); + } + + public void setPictureMode(String value) throws BenqProjectorCommandException, BenqProjectorException { + sendCommand(String.format("appmod=%s", value)); + } + + /* + * Aspect Ratio + */ + public @Nullable String getAspectRatio() throws BenqProjectorCommandException, BenqProjectorException { + return queryString("asp=?"); + } + + public void setAspectRatio(String value) throws BenqProjectorCommandException, BenqProjectorException { + sendCommand(String.format("asp=%s", value)); + } + + /* + * Blank Screen + */ + public Switch getBlank() throws BenqProjectorCommandException, BenqProjectorException { + return (queryString("blank=?").contains("on") ? Switch.ON : Switch.OFF); + } + + public void setBlank(Switch value) throws BenqProjectorCommandException, BenqProjectorException { + sendCommand(String.format("blank=%s", (value == Switch.ON ? "on" : "off"))); + } + + /* + * Freeze + */ + public Switch getFreeze() throws BenqProjectorCommandException, BenqProjectorException { + return (queryString("freeze=?").contains("on") ? Switch.ON : Switch.OFF); + } + + public void setFreeze(Switch value) throws BenqProjectorCommandException, BenqProjectorException { + sendCommand(String.format("freeze=%s", (value == Switch.ON ? "on" : "off"))); + } + + /* + * Direct Command + */ + public void sendDirectCommand(String value) throws BenqProjectorCommandException, BenqProjectorException { + sendCommand(value); + } + + /* + * Lamp Time (hours) - get from cache + */ + public int getLampTime() throws BenqProjectorCommandException, BenqProjectorException { + Integer lampHours = cachedLampHours.getValue(); + + if (lampHours != null) { + return lampHours.intValue(); + } else { + throw new BenqProjectorCommandException("cachedLampHours returned null"); + } + } + + /* + * Get Lamp Time + */ + private @Nullable Integer queryLamp() { + try { + return Integer.valueOf(queryInt("ltim=?")); + } catch (BenqProjectorCommandException | BenqProjectorException e) { + logger.debug("Error executing command ltim=?", e); + return null; + } + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorException.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorException.java new file mode 100644 index 0000000000000..3d75404bdd3da --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception for BenQ projector errors. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class BenqProjectorException extends Exception { + + private static final long serialVersionUID = -8048415193494625295L; + + public BenqProjectorException(String message) { + super(message); + } + + public BenqProjectorException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorHandlerFactory.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorHandlerFactory.java new file mode 100644 index 0000000000000..55013a808d2db --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/BenqProjectorHandlerFactory.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal; + +import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.benqprojector.internal.handler.BenqProjectorHandler; +import org.openhab.core.io.transport.serial.SerialPortManager; +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; + +/** + * The {@link BenqProjectorHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.benqprojector", service = ThingHandlerFactory.class) +public class BenqProjectorHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PROJECTOR_SERIAL, + THING_TYPE_PROJECTOR_TCP); + private final SerialPortManager serialPortManager; + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Activate + public BenqProjectorHandlerFactory(final @Reference SerialPortManager serialPortManager) { + this.serialPortManager = serialPortManager; + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_PROJECTOR_SERIAL.equals(thingTypeUID) || THING_TYPE_PROJECTOR_TCP.equals(thingTypeUID)) { + return new BenqProjectorHandler(thing, serialPortManager); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/configuration/BenqProjectorConfiguration.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/configuration/BenqProjectorConfiguration.java new file mode 100644 index 0000000000000..e028aef916c42 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/configuration/BenqProjectorConfiguration.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal.configuration; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link BenqProjectorConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class BenqProjectorConfiguration { + + /** + * Serial port used for communication. + */ + public String serialPort = ""; + + /** + * Host or IP address used for communication over a TCP link (if serialPort is not set). + */ + public String host = ""; + + /** + * Port used for communication over a TCP link (if serialPort is not set). + */ + public int port; + + /** + * Polling interval to refresh states. + */ + public int pollingInterval; +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorConnector.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorConnector.java new file mode 100644 index 0000000000000..d76117f21830d --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorConnector.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal.connector; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.benqprojector.internal.BenqProjectorException; + +/** + * Base class for BenQ projector communication. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public interface BenqProjectorConnector { + public static final int TIMEOUT_MS = 5 * 1000; + + public static final String START = "\r*"; + public static final String END = "#\r"; + public static final String BLANK = ""; + + /** + * Procedure for connecting to projector. + * + * @throws BenqProjectorException + */ + void connect() throws BenqProjectorException; + + /** + * Procedure for disconnecting to projector controller. + * + * @throws BenqProjectorException + */ + void disconnect() throws BenqProjectorException; + + /** + * Procedure for sending raw data to projector. + * + * @param data + * Message to send. + * + * @throws BenqProjectorException + */ + String sendMessage(String data) throws BenqProjectorException; + + /** + * Common method called by the Serial or Tcp connector to send the message to the projector, wait for a response and + * return it after processing. + * + * @param data + * Message to send. + * @param in + * The connector's input stream. + * @param out + * The connector's output stream. + * + * @throws BenqProjectorException + */ + default String sendMsgReadResp(String data, @Nullable InputStream in, @Nullable OutputStream out) + throws IOException, BenqProjectorException { + String resp = BLANK; + + if (in != null && out != null) { + out.write((START + data + END).getBytes(StandardCharsets.US_ASCII)); + out.flush(); + + long startTime = System.currentTimeMillis(); + long elapsedTime = 0; + + while (elapsedTime < TIMEOUT_MS) { + int availableBytes = in.available(); + if (availableBytes > 0) { + byte[] tmpData = new byte[availableBytes]; + int readBytes = in.read(tmpData, 0, availableBytes); + resp = resp.concat(new String(tmpData, 0, readBytes, StandardCharsets.US_ASCII)); + if (resp.contains(END)) { + return resp.replaceAll("[\\r\\n*#>]", BLANK).replace(data, BLANK); + } + } else { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new BenqProjectorException(e); + } + } + + elapsedTime = System.currentTimeMillis() - startTime; + } + } + return resp; + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorSerialConnector.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorSerialConnector.java new file mode 100644 index 0000000000000..b72c6d95e738a --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorSerialConnector.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal.connector; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.benqprojector.internal.BenqProjectorException; +import org.openhab.core.io.transport.serial.PortInUseException; +import org.openhab.core.io.transport.serial.SerialPort; +import org.openhab.core.io.transport.serial.SerialPortEvent; +import org.openhab.core.io.transport.serial.SerialPortEventListener; +import org.openhab.core.io.transport.serial.SerialPortIdentifier; +import org.openhab.core.io.transport.serial.SerialPortManager; +import org.openhab.core.io.transport.serial.UnsupportedCommOperationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Connector for serial port communication. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class BenqProjectorSerialConnector implements BenqProjectorConnector, SerialPortEventListener { + + private final Logger logger = LoggerFactory.getLogger(BenqProjectorSerialConnector.class); + private final String serialPortName; + private final SerialPortManager serialPortManager; + + private @Nullable InputStream in = null; + private @Nullable OutputStream out = null; + private @Nullable SerialPort serialPort = null; + + public BenqProjectorSerialConnector(SerialPortManager serialPortManager, String serialPort) { + this.serialPortManager = serialPortManager; + this.serialPortName = serialPort; + } + + @Override + public void connect() throws BenqProjectorException { + try { + logger.debug("Open connection to serial port '{}'", serialPortName); + + SerialPortIdentifier serialPortIdentifier = serialPortManager.getIdentifier(serialPortName); + + if (serialPortIdentifier == null) { + throw new IOException("Unknown serial port"); + } + SerialPort serialPort = serialPortIdentifier.open(this.getClass().getName(), 2000); + serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); + serialPort.enableReceiveThreshold(1); + serialPort.disableReceiveTimeout(); + + InputStream in = serialPort.getInputStream(); + OutputStream out = serialPort.getOutputStream(); + + if (in != null && out != null) { + out.flush(); + if (in.markSupported()) { + in.reset(); + } + + serialPort.notifyOnDataAvailable(true); + + this.serialPort = serialPort; + this.in = in; + this.out = out; + } + } catch (PortInUseException | UnsupportedCommOperationException | IOException e) { + throw new BenqProjectorException(e); + } + } + + @Override + public void disconnect() throws BenqProjectorException { + InputStream in = this.in; + OutputStream out = this.out; + SerialPort serialPort = this.serialPort; + + if (out != null) { + logger.debug("Close serial out stream"); + try { + out.close(); + } catch (IOException e) { + logger.debug("Error occurred when closing serial out stream: {}", e.getMessage()); + } + this.out = null; + } + if (in != null) { + logger.debug("Close serial in stream"); + try { + in.close(); + } catch (IOException e) { + logger.debug("Error occurred when closing serial in stream: {}", e.getMessage()); + } + this.in = null; + } + if (serialPort != null) { + logger.debug("Close serial port"); + serialPort.close(); + serialPort.removeEventListener(); + this.serialPort = null; + } + + logger.debug("Closed"); + } + + @Override + public String sendMessage(String data) throws BenqProjectorException { + InputStream in = this.in; + OutputStream out = this.out; + + if (in == null || out == null) { + connect(); + in = this.in; + out = this.out; + } + + try { + if (in != null && out != null) { + // flush input stream + if (in.markSupported()) { + in.reset(); + } else { + while (in.available() > 0) { + int availableBytes = in.available(); + + if (availableBytes > 0) { + byte[] tmpData = new byte[availableBytes]; + in.read(tmpData, 0, availableBytes); + } + } + } + return sendMsgReadResp(data, in, out); + } else { + return BLANK; + } + } catch (IOException e) { + logger.debug("IO error occurred...reconnect and resend once: {}", e.getMessage()); + disconnect(); + connect(); + + try { + return sendMsgReadResp(data, in, out); + } catch (IOException e1) { + throw new BenqProjectorException(e); + } + } + } + + @Override + public void serialEvent(SerialPortEvent arg0) { + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorTcpConnector.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorTcpConnector.java new file mode 100644 index 0000000000000..af7861972e275 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/connector/BenqProjectorTcpConnector.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal.connector; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.benqprojector.internal.BenqProjectorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Connector for TCP communication. + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class BenqProjectorTcpConnector implements BenqProjectorConnector { + + private final Logger logger = LoggerFactory.getLogger(BenqProjectorTcpConnector.class); + private final String ip; + private final int port; + + private @Nullable Socket socket = null; + private @Nullable InputStream in = null; + private @Nullable OutputStream out = null; + + public BenqProjectorTcpConnector(String ip, int port) { + this.ip = ip; + this.port = port; + } + + @Override + public void connect() throws BenqProjectorException { + logger.debug("Open connection to address'{}:{}'", ip, port); + + try { + Socket socket = new Socket(ip, port); + this.socket = socket; + in = socket.getInputStream(); + out = socket.getOutputStream(); + } catch (IOException e) { + throw new BenqProjectorException(e); + } + } + + @Override + public void disconnect() throws BenqProjectorException { + OutputStream out = this.out; + + if (out != null) { + logger.debug("Close tcp out stream"); + try { + out.close(); + } catch (IOException e) { + logger.debug("Error occurred when closing tcp out stream: {}", e.getMessage()); + } + } + + InputStream in = this.in; + if (in != null) { + logger.debug("Close tcp in stream"); + try { + in.close(); + } catch (IOException e) { + logger.debug("Error occurred when closing tcp in stream: {}", e.getMessage()); + } + } + + Socket socket = this.socket; + if (socket != null) { + logger.debug("Closing socket"); + try { + socket.close(); + } catch (IOException e) { + logger.debug("Error occurred when closing tcp socket: {}", e.getMessage()); + } + } + + this.socket = null; + this.out = null; + this.in = null; + + logger.debug("Closed"); + } + + @Override + public String sendMessage(String data) throws BenqProjectorException { + InputStream in = this.in; + OutputStream out = this.out; + + if (in == null || out == null) { + connect(); + in = this.in; + out = this.out; + } + + try { + if (in != null) { + // flush input stream + if (in.markSupported()) { + in.reset(); + } else { + while (in.available() > 0) { + int availableBytes = in.available(); + + if (availableBytes > 0) { + byte[] tmpData = new byte[availableBytes]; + in.read(tmpData, 0, availableBytes); + } + } + } + return sendMsgReadResp(data, in, out); + } else { + return BLANK; + } + } catch (IOException e) { + logger.debug("IO error occurred...reconnect and resend once: {}", e.getMessage()); + disconnect(); + connect(); + + try { + return sendMsgReadResp(data, in, out); + } catch (IOException e1) { + throw new BenqProjectorException(e); + } + } + } +} diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/RoombaConfiguration.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/enums/Switch.java similarity index 67% rename from bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/RoombaConfiguration.java rename to bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/enums/Switch.java index 66d9be891bc53..04cab831f613b 100644 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/RoombaConfiguration.java +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/enums/Switch.java @@ -10,18 +10,17 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.irobot.internal; +package org.openhab.binding.benqprojector.internal.enums; import org.eclipse.jdt.annotation.NonNullByDefault; /** - * Roomba Thing configuration - * - * @author Pavel Fedin - Initial contribution + * Valid values for BenQ switch commands. * + * @author Michael Lobstein - Initial contribution */ @NonNullByDefault -public class RoombaConfiguration { - public String ipaddress = ""; - public String password = ""; +public enum Switch { + ON, + OFF; } diff --git a/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/handler/BenqProjectorHandler.java b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/handler/BenqProjectorHandler.java new file mode 100644 index 0000000000000..63fb9c3c0b897 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/java/org/openhab/binding/benqprojector/internal/handler/BenqProjectorHandler.java @@ -0,0 +1,291 @@ +/** + * Copyright (c) 2010-2021 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.benqprojector.internal.handler; + +import static org.openhab.binding.benqprojector.internal.BenqProjectorBindingConstants.*; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.benqprojector.internal.BenqProjectorCommandException; +import org.openhab.binding.benqprojector.internal.BenqProjectorCommandType; +import org.openhab.binding.benqprojector.internal.BenqProjectorDevice; +import org.openhab.binding.benqprojector.internal.BenqProjectorException; +import org.openhab.binding.benqprojector.internal.configuration.BenqProjectorConfiguration; +import org.openhab.binding.benqprojector.internal.enums.Switch; +import org.openhab.core.io.transport.serial.SerialPortManager; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link BenqProjectorHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * Based on 'epsonprojector' originally by Pauli Anttila & Yannick Schaus + * + * @author Michael Lobstein - Initial contribution + */ +@NonNullByDefault +public class BenqProjectorHandler extends BaseThingHandler { + private static final int DEFAULT_POLLING_INTERVAL_SEC = 10; + + private final Logger logger = LoggerFactory.getLogger(BenqProjectorHandler.class); + private final SerialPortManager serialPortManager; + + private @Nullable ScheduledFuture pollingJob; + private Optional device = Optional.empty(); + + private boolean isPowerOn = false; + private int pollingInterval = DEFAULT_POLLING_INTERVAL_SEC; + + public BenqProjectorHandler(Thing thing, SerialPortManager serialPortManager) { + super(thing); + this.serialPortManager = serialPortManager; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + String channelId = channelUID.getId(); + if (command instanceof RefreshType) { + Channel channel = this.thing.getChannel(channelUID); + if (channel != null && getThing().getStatus() == ThingStatus.ONLINE) { + updateChannelState(channel); + } + } else { + BenqProjectorCommandType benqCommand = BenqProjectorCommandType.getCommandType(channelId); + sendDataToDevice(benqCommand, command); + } + } + + @Override + public void initialize() { + BenqProjectorConfiguration config = getConfigAs(BenqProjectorConfiguration.class); + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_PROJECTOR_SERIAL.equals(thingTypeUID)) { + device = Optional.of(new BenqProjectorDevice(serialPortManager, config)); + } else if (THING_TYPE_PROJECTOR_TCP.equals(thingTypeUID)) { + device = Optional.of(new BenqProjectorDevice(config)); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); + return; + } + + pollingInterval = config.pollingInterval; + updateStatus(ThingStatus.UNKNOWN); + schedulePollingJob(); + } + + /** + * Schedule the polling job + */ + private void schedulePollingJob() { + cancelPollingJob(); + + pollingJob = scheduler.scheduleWithFixedDelay(() -> { + List channels = this.thing.getChannels(); + for (Channel channel : channels) { + // only query power when projector is off + if (isPowerOn || channel.getUID().getId().equals(CHANNEL_TYPE_POWER)) { + updateChannelState(channel); + } + } + }, 0, (pollingInterval > 0) ? pollingInterval : DEFAULT_POLLING_INTERVAL_SEC, TimeUnit.SECONDS); + } + + /** + * Cancel the polling job + */ + private void cancelPollingJob() { + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + this.pollingJob = null; + } + } + + @Override + public void dispose() { + cancelPollingJob(); + closeConnection(); + super.dispose(); + } + + private void updateChannelState(Channel channel) { + try { + if (!isLinked(channel.getUID()) && !channel.getUID().getId().equals(CHANNEL_TYPE_POWER)) { + return; + } + + BenqProjectorCommandType benqCommand = BenqProjectorCommandType.getCommandType(channel.getUID().getId()); + + State state = queryDataFromDevice(benqCommand); + + if (state != null) { + if (isLinked(channel.getUID())) { + updateState(channel.getUID(), state); + } + // the first valid response will cause the thing to go ONLINE + if (state != UnDefType.UNDEF) { + updateStatus(ThingStatus.ONLINE); + } + } + } catch (IllegalArgumentException e) { + logger.warn("Unknown channel {}", channel.getUID().getId()); + } + } + + @Nullable + private State queryDataFromDevice(BenqProjectorCommandType commandType) { + BenqProjectorDevice remoteController = device.get(); + + try { + if (!remoteController.isConnected()) { + remoteController.connect(); + } + + switch (commandType) { + case POWER: + Switch powerStatus = remoteController.getPowerStatus(); + if (powerStatus == Switch.ON) { + isPowerOn = true; + return OnOffType.ON; + } else { + isPowerOn = false; + return OnOffType.OFF; + } + case SOURCE: + String source = remoteController.getSource(); + if (source != null) { + return new StringType(source); + } else { + return UnDefType.UNDEF; + } + case PICTURE_MODE: + String picturemode = remoteController.getPictureMode(); + if (picturemode != null) { + return new StringType(picturemode); + } else { + return UnDefType.UNDEF; + } + case ASPECT_RATIO: + String aspectratio = remoteController.getAspectRatio(); + if (aspectratio != null) { + return new StringType(aspectratio); + } else { + return UnDefType.UNDEF; + } + case FREEZE: + Switch freeze = remoteController.getFreeze(); + return freeze == Switch.ON ? OnOffType.ON : OnOffType.OFF; + case BLANK: + Switch blank = remoteController.getBlank(); + return blank == Switch.ON ? OnOffType.ON : OnOffType.OFF; + case DIRECTCMD: + break; + case LAMP_TIME: + int lampTime = remoteController.getLampTime(); + return new DecimalType(lampTime); + default: + logger.warn("Unknown '{}' command!", commandType); + return UnDefType.UNDEF; + } + } catch (BenqProjectorCommandException e) { + logger.debug("Error executing command '{}', {}", commandType, e.getMessage()); + return UnDefType.UNDEF; + } catch (BenqProjectorException e) { + logger.debug("Couldn't execute command '{}', {}", commandType, e.getMessage()); + closeConnection(); + return null; + } + + return UnDefType.UNDEF; + } + + private void sendDataToDevice(BenqProjectorCommandType commandType, Command command) { + BenqProjectorDevice remoteController = device.get(); + + try { + if (!remoteController.isConnected()) { + remoteController.connect(); + } + + switch (commandType) { + case POWER: + if (command == OnOffType.ON) { + remoteController.setPower(Switch.ON); + isPowerOn = true; + } else { + remoteController.setPower(Switch.OFF); + isPowerOn = false; + } + break; + case SOURCE: + remoteController.setSource(command.toString()); + break; + case PICTURE_MODE: + remoteController.setPictureMode(command.toString()); + break; + case ASPECT_RATIO: + remoteController.setAspectRatio(command.toString()); + break; + case FREEZE: + remoteController.setFreeze(command == OnOffType.ON ? Switch.ON : Switch.OFF); + break; + case BLANK: + remoteController.setBlank(command == OnOffType.ON ? Switch.ON : Switch.OFF); + break; + case DIRECTCMD: + remoteController.sendDirectCommand(command.toString()); + break; + default: + logger.warn("Unknown '{}' command!", commandType); + break; + } + } catch (BenqProjectorCommandException e) { + logger.debug("Error executing command '{}', {}", commandType, e.getMessage()); + } catch (BenqProjectorException e) { + logger.warn("Couldn't execute command '{}', {}", commandType, e.getMessage()); + closeConnection(); + } + } + + private void closeConnection() { + BenqProjectorDevice remoteController = device.get(); + try { + logger.debug("Closing connection to device '{}'", this.thing.getUID()); + remoteController.disconnect(); + updateStatus(ThingStatus.OFFLINE); + } catch (BenqProjectorException e) { + logger.debug("Error occurred when closing connection to device '{}'", this.thing.getUID(), e); + } + } +} diff --git a/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..d429d87e6d0e2 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + BenQ Projector Binding + This binding is compatible with BenQ projectors + + diff --git a/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..4bf9876508bc8 --- /dev/null +++ b/bundles/org.openhab.binding.benqprojector/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,168 @@ + + + + + + A BenQ projector connected via a serial port + + + + + + + + + + + + + + + + serial-port + Serial Port to Use for Connecting to the BenQ Projector + + + + Configures How Often to Poll the Projector for Updates (5-60; Default 10) + 10 + + + + + + + + A BenQ projector connected via the built-in ethernet port or a serial over + IP device + + + + + + + + + + + + + + + + network-address + IP address for the projector or serial over IP device + + + + Port for the projector or serial over IP device + 8000 + + + + Configures How Often to Poll the Projector for Updates (5-60; Default 10) + 10 + + + + + + + String + + Retrieve or Set the Input Source + + + + + + + + + + + + + + String + + Retrieve or Set the Picture Mode + + + + + + + + + + + + + + + + + + + + + + + String + + Retrieve or Set the Aspect Ratio + + + + + + + + + + + + Switch + + Turn the Freeze Image Mode On or Off + + + Switch + + Turn the Screen Blank On or Off + + + String + + Send a Command Directly to the Projector + + + + + + + + + + + + + + + + + + + + + Number + + Retrieves the Lamp Hours + + + + diff --git a/bundles/org.openhab.binding.comfoair/README.md b/bundles/org.openhab.binding.comfoair/README.md index f5b96fe6c8e33..0965ddcb9eb86 100644 --- a/bundles/org.openhab.binding.comfoair/README.md +++ b/bundles/org.openhab.binding.comfoair/README.md @@ -27,11 +27,14 @@ sudo usermod -a -G dialout openhab ## Supported Things -Only a single generic thing type is supported by the binding: - -|Thing Type ID |Description | -|--------------|---------------------------------------------------------------------| -|comfoair |A ComfoAir ventilation system connected via RS232 serial connection. | +The binding supports thing types for different device types. +They only differ in the available channels, where the generic *comfoair* thing type supports all available channels. +If there is no thing type that matches your specific device you can safely choose the *comfoair* type. + +|Thing Type ID |Description | +|--------------|-----------------------------------------------------------------------------| +|comfoair |A ComfoAir ventilation system connected via RS232 serial connection. | +|WHR930 |Thing type restricted to the data points available for the StorkAir WHR930. | ## Discovery diff --git a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirBindingConstants.java b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirBindingConstants.java index e031e2d038de0..45d2264f89ef3 100644 --- a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirBindingConstants.java +++ b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirBindingConstants.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.comfoair.internal; +import java.util.Set; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -27,6 +29,10 @@ public class ComfoAirBindingConstants { private static final String BINDING_ID = "comfoair"; public static final ThingTypeUID THING_TYPE_COMFOAIR_GENERIC = new ThingTypeUID(BINDING_ID, "comfoair"); + public static final ThingTypeUID THING_TYPE_COMFOAIR_WHR930 = new ThingTypeUID(BINDING_ID, "WHR930"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_COMFOAIR_GENERIC, + THING_TYPE_COMFOAIR_WHR930); // Thing properties public static final String PROPERTY_SOFTWARE_MAIN_VERSION = "SOFTWARE_VERSION_MAIN"; diff --git a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirHandler.java b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirHandler.java index c384c6626a229..c356d9fa2139e 100644 --- a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirHandler.java +++ b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirHandler.java @@ -88,7 +88,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { .getAffectedReadCommands(channelId, keysToUpdate); if (!affectedReadCommands.isEmpty()) { - Runnable updateThread = new AffectedItemsUpdateThread(affectedReadCommands); + Runnable updateThread = new AffectedItemsUpdateThread(affectedReadCommands, keysToUpdate); affectedItemsPoller = scheduler.schedule(updateThread, 3, TimeUnit.SECONDS); } } else { @@ -273,7 +273,8 @@ private State sendCommand(ComfoAirCommand command, String commandKey) { } if (value instanceof UnDefType) { if (logger.isWarnEnabled()) { - logger.warn("unexpected value for DATA: {}", ComfoAirSerialConnector.dumpData(response)); + logger.warn("unexpected value for key '{}'. DATA: {}", commandKey, + ComfoAirSerialConnector.dumpData(response)); } } return value; @@ -320,9 +321,11 @@ public void pullDeviceProperties() { private class AffectedItemsUpdateThread implements Runnable { private Collection affectedReadCommands; + private Set linkedChannels; - public AffectedItemsUpdateThread(Collection affectedReadCommands) { + public AffectedItemsUpdateThread(Collection affectedReadCommands, Set linkedChannels) { this.affectedReadCommands = affectedReadCommands; + this.linkedChannels = linkedChannels; } @Override @@ -334,8 +337,10 @@ public void run() { for (ComfoAirCommandType commandType : commandTypes) { String commandKey = commandType.getKey(); - State state = sendCommand(readCommand, commandKey); - updateState(commandKey, state); + if (linkedChannels.contains(commandKey)) { + State state = sendCommand(readCommand, commandKey); + updateState(commandKey, state); + } } } } diff --git a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirHandlerFactory.java b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirHandlerFactory.java index 51be0308663fb..f0822918c55a6 100644 --- a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirHandlerFactory.java +++ b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirHandlerFactory.java @@ -12,9 +12,6 @@ */ package org.openhab.binding.comfoair.internal; -import java.util.Collections; -import java.util.Set; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.io.transport.serial.SerialPortManager; @@ -36,9 +33,6 @@ @Component(configurationPid = "binding.comfoair", service = ThingHandlerFactory.class) public class ComfoAirHandlerFactory extends BaseThingHandlerFactory { - private static final Set SUPPORTED_THING_TYPES_UIDS = Collections - .singleton(ComfoAirBindingConstants.THING_TYPE_COMFOAIR_GENERIC); - private @NonNullByDefault({}) SerialPortManager serialPortManager; @Reference @@ -52,14 +46,15 @@ protected void unsetSerialPortManager(final SerialPortManager serialPortManager) @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + return ComfoAirBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); } @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - if (ComfoAirBindingConstants.THING_TYPE_COMFOAIR_GENERIC.equals(thingTypeUID)) { + if (ComfoAirBindingConstants.THING_TYPE_COMFOAIR_GENERIC.equals(thingTypeUID) + || ComfoAirBindingConstants.THING_TYPE_COMFOAIR_WHR930.equals(thingTypeUID)) { return new ComfoAirHandler(thing, serialPortManager); } diff --git a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirSerialConnector.java b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirSerialConnector.java index 69945f3b8b003..f75ea88adfeda 100644 --- a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirSerialConnector.java +++ b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/ComfoAirSerialConnector.java @@ -511,7 +511,9 @@ private int[] buildRequestData(ComfoAirCommand command, int[] preRequestData) { if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) { System.arraycopy(preRequestData, 0, newRequestData, 0, 6); - System.arraycopy(preRequestData, 10, newRequestData, 6, newRequestData.length - 6); + if (preRequestData.length > 10) { + System.arraycopy(preRequestData, 10, newRequestData, 6, newRequestData.length - 6); + } newRequestData[dataPosition] = requestValue; } else { return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY; diff --git a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/datatypes/DataTypeMessage.java b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/datatypes/DataTypeMessage.java index fa7f5f130da8b..6de5be2bfca83 100644 --- a/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/datatypes/DataTypeMessage.java +++ b/bundles/org.openhab.binding.comfoair/src/main/java/org/openhab/binding/comfoair/internal/datatypes/DataTypeMessage.java @@ -50,8 +50,8 @@ public State convertToState(int @Nullable [] data, ComfoAirCommandType commandTy if (readReplyDataPos != null) { int errorAlo = data[readReplyDataPos[0]]; int errorE = data[readReplyDataPos[1]]; - int errorEA = data[readReplyDataPos[2]]; - int errorAhi = data[readReplyDataPos[3]]; + int errorEA = (data.length > 9) ? data[readReplyDataPos[2]] : -1; + int errorAhi = (data.length > 9) ? data[readReplyDataPos[3]] : -1; StringBuilder errorCode = new StringBuilder(); diff --git a/bundles/org.openhab.binding.comfoair/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.comfoair/src/main/resources/OH-INF/config/config.xml new file mode 100644 index 0000000000000..7210e8a486080 --- /dev/null +++ b/bundles/org.openhab.binding.comfoair/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,20 @@ + + + + + + + serial-port + Serial port that the ComfoAir is connected to + + + + Refresh interval in seconds + 60 + + + diff --git a/bundles/org.openhab.binding.comfoair/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.comfoair/src/main/resources/OH-INF/thing/thing-types.xml index d600617279ba3..ec2b1203dbca6 100644 --- a/bundles/org.openhab.binding.comfoair/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.comfoair/src/main/resources/OH-INF/thing/thing-types.xml @@ -41,18 +41,32 @@ - - - - serial-port - Serial port that the ComfoAir is connected to - - - - Refresh interval in seconds - 60 - - + + + + + + + Provides a generic access to a Zehnder WHR930 ventilation device + + + + + + + + + + + + + + + + + + + @@ -82,6 +96,23 @@ + + + + + + + + + + + + + + + + + @@ -103,6 +134,21 @@ + + + + + + + + + + + + + + + @@ -117,6 +163,19 @@ + + + + + + + + + + + + + @@ -189,6 +248,18 @@ + + + + + + + + + + + + @@ -205,6 +276,18 @@ + + + + + + + + + + + + @@ -219,6 +302,18 @@ + + + + + + + + + + + + @@ -233,6 +328,17 @@ + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java index b3b455a9a003c..ad2f51409b576 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/GroupThingHandler.java @@ -129,8 +129,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } Integer bri = newGroupAction.bri; - if (bri != null && bri > 0) { - newGroupAction.on = true; + if (bri != null) { + newGroupAction.on = (bri > 0); } sendCommand(newGroupAction, command, channelUID, null); diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java index a25c62af95641..25a267bcba7b0 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/LightThingHandler.java @@ -96,7 +96,8 @@ public class LightThingHandler extends DeconzBaseThingHandler { */ private LightState lightStateCache = new LightState(); private LightState lastCommand = new LightState(); - private int onTime = 0; // in 0.1s + @Nullable + private Integer onTime = null; // in 0.1s private String colorMode = ""; // set defaults, we can override them later if we receive better values diff --git a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeThermostatBridgeHandler.java b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeThermostatBridgeHandler.java index f6b399bf6f867..18e4eb48bd932 100644 --- a/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeThermostatBridgeHandler.java +++ b/bundles/org.openhab.binding.ecobee/src/main/java/org/openhab/binding/ecobee/internal/handler/EcobeeThermostatBridgeHandler.java @@ -277,7 +277,7 @@ private void handleThermostatCommand(ChannelUID channelUID, Command command) { String channelId = channelUID.getIdWithoutGroup(); String groupId = channelUID.getGroupId(); if (groupId == null) { - logger.info("Can't handle command because channel's groupId is null"); + logger.info("Can't handle command '{}' because channel's groupId is null", command); return; } ThermostatDTO thermostat = new ThermostatDTO(); @@ -317,7 +317,7 @@ private void handleThermostatCommand(ChannelUID channelUID, Command command) { } private void setField(Field field, Object object, Command command) { - logger.info("Setting field '{}.{}' to value '{}'", object.getClass().getSimpleName().toLowerCase(), + logger.debug("Setting field '{}.{}' to value '{}'", object.getClass().getSimpleName().toLowerCase(), field.getName(), command); Class fieldClass = field.getType(); try { diff --git a/bundles/org.openhab.binding.epsonprojector/README.md b/bundles/org.openhab.binding.epsonprojector/README.md index 4f80e33217317..267218070792e 100644 --- a/bundles/org.openhab.binding.epsonprojector/README.md +++ b/bundles/org.openhab.binding.epsonprojector/README.md @@ -21,14 +21,18 @@ All settings are through thing configuration parameters. The `projector-serial` thing has the following configuration parameters: -- _serialPort_: Serial port device name that is connected to the Epson projector to control, e.g. COM1 on Windows, /dev/ttyS0 on Linux or /dev/tty.PL2303-0000103D on Mac -- _pollingInterval_: Polling interval in seconds to update channel states | 5-60 seconds; default 10 seconds +| Parameter | Name | Description | Required | +|-----------------|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|----------| +| serialPort | Serial Port | Serial port device name that is connected to the Epson projector to control, e.g. COM1 on Windows, /dev/ttyS0 on Linux or /dev/tty.PL2303-0000103D on Mac. | yes | +| pollingInterval | Polling Interval | Polling interval in seconds to update channel states, range 5-60 seconds; default 10 seconds. | no | The `projector-tcp` thing has the following configuration parameters: -- _host_: IP address for the projector or serial over IP device -- _port_: Port for the projector or serial over IP device; default 3629 for projectors with built-in ethernet connector or Wi-Fi -- _pollingInterval_: Polling interval in seconds to update channel states | 5-60 seconds; default 10 seconds +| Parameter | Name | Description | Required | +|-----------------|------------------|-------------------------------------------------------------------------------------------------------------------------|----------| +| host | Host Name | Host Name or IP address for the projector or serial over IP device. | yes | +| port | Port | Port for the projector or serial over IP device; default 3629 for projectors with built-in ethernet connector or Wi-Fi. | yes | +| pollingInterval | Polling Interval | Polling interval in seconds to update channel states, range 5-60 seconds; default 10 seconds. | no | Some notes: @@ -87,12 +91,12 @@ Some notes: things/epson.things: -```java +``` // serial port connection epsonprojector:projector-serial:hometheater "Projector" [ serialPort="COM5", pollingInterval=10 ] // direct IP or serial over IP connection -epsonprojector:projector-tcp:hometheater "Projector" [ host="192.168.0.10", port=3629, pollingInterval=10 ] +epsonprojector:projector-tcp:hometheater "Projector" [ host="192.168.0.10", port=3629, pollingInterval=10 ] ``` diff --git a/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml index 611c9368e392e..6fb3e30778b52 100644 --- a/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.epsonprojector/src/main/resources/OH-INF/thing/thing-types.xml @@ -46,7 +46,7 @@ Serial Port to Use for Connecting to the Epson Projector - + Configures How Often to Poll the Projector for Updates (5-60; Default 10) 10 @@ -104,7 +104,7 @@ 3629 - + Configures How Often to Poll the Projector for Updates (5-60; Default 10) 10 diff --git a/bundles/org.openhab.binding.etherrain/README.md b/bundles/org.openhab.binding.etherrain/README.md index 50d077f841aaf..4f4338a199804 100644 --- a/bundles/org.openhab.binding.etherrain/README.md +++ b/bundles/org.openhab.binding.etherrain/README.md @@ -48,9 +48,9 @@ Finally, there are commands to execute and clear the commands: items: ``` -String SprinkerCommandStatus "Command Status [%s]" (gMain) { channel="etherrain:etherrain:sprinkler0:commandstatus" } -String SprinkerOperatingStatus "Operating Status [%s]" (gMain) { channel="etherrain:etherrain:sprinkler0:operatingstatus" } -String SprinkerOperatingResult "Operating Result [%s]" (gMain) { channel="etherrain:etherrain:sprinkler0:operatingresult" } +String SprinklerCommandStatus "Command Status [%s]" (gMain) { channel="etherrain:etherrain:sprinkler0:commandstatus" } +String SprinklerOperatingStatus "Operating Status [%s]" (gMain) { channel="etherrain:etherrain:sprinkler0:operatingstatus" } +String SprinklerOperatingResult "Operating Result [%s]" (gMain) { channel="etherrain:etherrain:sprinkler0:operatingresult" } String SprinklerActiveZone "Active Zone [%s]" (gMain) { channel="etherrain:etherrain:sprinkler0:relayindex" } Switch SprinklerRainSensor (gMain) { channel="etherrain:etherrain:sprinkler0:rainsensor" } diff --git a/bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/Client.java b/bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/Client.java index 2e4e5a016b1d1..4c3e9cd2b26e0 100644 --- a/bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/Client.java +++ b/bundles/org.openhab.binding.fmiweather/src/main/java/org/openhab/binding/fmiweather/internal/client/Client.java @@ -71,7 +71,7 @@ public class Client { private static final Map NAMESPACES = new HashMap<>(); static { - NAMESPACES.put("target", "http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0"); + NAMESPACES.put("target", "http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.1"); NAMESPACES.put("gml", "http://www.opengis.net/gml/3.2"); NAMESPACES.put("xlink", "http://www.w3.org/1999/xlink"); NAMESPACES.put("ows", "http://www.opengis.net/ows/1.1"); @@ -374,7 +374,8 @@ private String[] queryNodeValues(XPathExpression expression, Object source) thro */ private String takeFirstOrError(String errorDescription, String[] values) throws FMIUnexpectedResponseException { if (values.length != 1) { - throw new FMIUnexpectedResponseException(String.format("No unique match found: %s", errorDescription)); + throw new FMIUnexpectedResponseException( + String.format("No unique match found: %s (found %d)", errorDescription, values.length)); } return values[0]; } diff --git a/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/AbstractFMIResponseParsingTest.java b/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/AbstractFMIResponseParsingTest.java index 397e46832ffc1..ef6b89fe623a0 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/AbstractFMIResponseParsingTest.java +++ b/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/AbstractFMIResponseParsingTest.java @@ -12,7 +12,7 @@ */ package org.openhab.binding.fmiweather; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.fail; import java.io.BufferedReader; import java.io.IOException; @@ -23,6 +23,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashSet; import java.util.Objects; import java.util.Set; @@ -109,6 +110,36 @@ protected boolean matchesSafely(Data dataValues) { return timestampMatcher.matches(dataValues.timestampsEpochSecs) && valuesMatcher.matches(dataValues.values); } + + @Override + protected void describeMismatchSafely(Data dataValues, @Nullable Description mismatchDescription) { + if (mismatchDescription == null) { + super.describeMismatchSafely(dataValues, mismatchDescription); + return; + } + if (!timestampMatcher.matches(dataValues.timestampsEpochSecs)) { + mismatchDescription.appendText("timestamps mismatch: "); + if (dataValues.timestampsEpochSecs[0] != start) { + mismatchDescription.appendText("start mismatch (was "); + mismatchDescription.appendValue(dataValues.timestampsEpochSecs[0]); + mismatchDescription.appendText(")"); + } else if (dataValues.timestampsEpochSecs.length != values.length) { + mismatchDescription.appendText("length mismatch (was "); + mismatchDescription.appendValue(dataValues.timestampsEpochSecs.length); + mismatchDescription.appendText(")"); + } else { + mismatchDescription.appendText("interval mismatch (was "); + Set intervals = new HashSet<>(); + for (int i = 1; i < values.length; i++) { + long interval = dataValues.timestampsEpochSecs[i] - dataValues.timestampsEpochSecs[i - 1]; + intervals.add(interval); + } + mismatchDescription.appendValue(intervals.toArray()); + mismatchDescription.appendText(")"); + } + } + mismatchDescription.appendText(", valuesMatch=").appendValue(valuesMatcher.matches(dataValues.values)); + } }; } diff --git a/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingExceptionReportTest.java b/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingExceptionReportTest.java index afa615c270176..d82da881f981d 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingExceptionReportTest.java +++ b/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingExceptionReportTest.java @@ -39,7 +39,7 @@ public void testErrorResponse() { } catch (FMIResponseException e) { // OK assertThat(e.getMessage(), is( - "Exception report (OperationParsingFailed): [Invalid time interval!, The start time is later than the end time., URI:\n\t\t\t/wfs?endtime=1900-03-10T20%3A10%3A00Z&fmisid=101023¶meters=t2m%2Crh%2Cwd_10min%2Cws_10min%2Cwg_10min%2Cp_sea&request=getFeature&service=WFS&starttime=2019-03-10T10%3A10%3A00Z&storedquery_id=fmi%3A%3Aobservations%3A%3Aweather%3A%3Amultipointcoverage×tep=60&version=2.0.0]")); + "Exception report (OperationParsingFailed): [Invalid time interval!, The start time is later than the end time., URI: /wfs?endtime=1900-03-10T20%3A10%3A00Z&fmisid=101023¶meters=t2m%2Crh%2Cwd_10min%2Cws_10min%2Cwg_10min%2Cp_sea&request=getFeature&service=WFS&starttime=2019-03-10T10%3A10%3A00Z&storedquery_id=fmi%3A%3Aobservations%3A%3Aweather%3A%3Amultipointcoverage×tep=60&version=2.0.0]")); return; } catch (Throwable e) { fail("Wrong exception, was " + e.getClass().getName()); diff --git a/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingMultiplePlacesTest.java b/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingMultiplePlacesTest.java index 938a728178157..5c4d4efd9d3ef 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingMultiplePlacesTest.java +++ b/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/FMIResponseParsingMultiplePlacesTest.java @@ -73,7 +73,6 @@ public void setUp() { } } - @SuppressWarnings("unchecked") @Test public void testLocationsMultiplePlacesObservations() { // locations @@ -82,7 +81,6 @@ public void testLocationsMultiplePlacesObservations() { hasItems(deeplyEqualTo(emasalo), deeplyEqualTo(kilpilahti), deeplyEqualTo(harabacka))); } - @SuppressWarnings("unchecked") @Test public void testLocationsMultiplePlacesForecasts() { // locations @@ -123,19 +121,20 @@ public void testParseObservationsMultipleData() { @Test public void testParseForecastsMultipleData() { + long start = 1622116800; Data temperature = forecastsMultiplePlacesResponse.getData(maarianhamina, "Temperature").get(); - assertThat(temperature, is(deeplyEqualTo(1553688000, 360, "3.84", "2.62", "2.26", "1.22", "5.47", "5.52", - "5.42", "4.78", "8.34", "7.15", null, null, null, null))); + assertThat(temperature, is(deeplyEqualTo(start, 360, "7.75", "7.94", "6.72", "8.22", "11.37", "9.69", "6.42", + "9.52", "11.04", "9.69", null, null, null, null))); Data temperature2 = forecastsMultiplePlacesResponse.getData(pointWithNoName, "Temperature").get(); - assertThat(temperature2, is(deeplyEqualTo(1553688000, 360, "1.54", "2.91", "2.41", "2.36", "4.22", "5.28", - "4.58", "4.0", "4.79", "5.4", null, null, null, null))); + assertThat(temperature2, is(deeplyEqualTo(start, 360, "7.46", "6.56", "6.2", "5.15", "5.05", "5.96", "6.2", + "5.94", "5.69", "5.47", null, null, null, null))); Data humidity = forecastsMultiplePlacesResponse.getData(maarianhamina, "Humidity").get(); - assertThat(humidity, is(deeplyEqualTo(1553688000, 360, "66.57", "87.38", "85.77", "96.3", "75.74", "81.7", - "86.78", "87.96", "70.86", "76.35", null, null, null, null))); + assertThat(humidity, is(deeplyEqualTo(start, 360, "93.76", "93.24", "98.22", "93.93", "75.78", "58.91", "80.42", + "54.11", "40.29", "46.42", null, null, null, null))); Data humidity2 = forecastsMultiplePlacesResponse.getData(pointWithNoName, "Humidity").get(); - assertThat(humidity2, is(deeplyEqualTo(1553688000, 360, "90.18", "86.22", "89.18", "89.43", "77.26", "78.55", - "83.36", "85.83", "80.82", "76.92", null, null, null, null))); + assertThat(humidity2, is(deeplyEqualTo(start, 360, "93.44", "95.3", "96.15", "93.77", "93.0", "82.1", "81.95", + "81.37", "85.41", "87.8", null, null, null, null))); } @Test diff --git a/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/ParsingStationsTest.java b/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/ParsingStationsTest.java index b0d3330ca892e..0d9907db3d500 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/ParsingStationsTest.java +++ b/bundles/org.openhab.binding.fmiweather/src/test/java/org/openhab/binding/fmiweather/ParsingStationsTest.java @@ -46,7 +46,7 @@ public void testParseStations() { new BigDecimal("25.549164"))), deeplyEqualTo(new Location("Parainen Utö", "100908", new BigDecimal("59.779094"), new BigDecimal("21.374788"))), - deeplyEqualTo(new Location("Lemland Nyhamn", "100909", new BigDecimal("59.959108"), - new BigDecimal("19.953736"))))); + deeplyEqualTo(new Location("Lemland Nyhamn", "100909", new BigDecimal("59.959194"), + new BigDecimal("19.953667"))))); } } diff --git a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/error1.xml b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/error1.xml index c3eafbe525ce4..c679f35206d06 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/error1.xml +++ b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/error1.xml @@ -1,17 +1,16 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.opengis.net/ows/1.1 http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd" + version="2.0.0" xml:lang="eng"> - - Invalid time interval! - The start time is later than the end time. - URI: - /wfs?endtime=1900-03-10T20%3A10%3A00Z&fmisid=101023&parameters=t2m%2Crh%2Cwd_10min%2Cws_10min%2Cwg_10min%2Cp_sea&request=getFeature&service=WFS&starttime=2019-03-10T10%3A10%3A00Z&storedquery_id=fmi%3A%3Aobservations%3A%3Aweather%3A%3Amultipointcoverage&timestep=60&version=2.0.0 + + Invalid time interval! + The start time is later than the end time. + URI: /wfs?endtime=1900-03-10T20%3A10%3A00Z&fmisid=101023&parameters=t2m%2Crh%2Cwd_10min%2Cws_10min%2Cwg_10min%2Cp_sea&request=getFeature&service=WFS&starttime=2019-03-10T10%3A10%3A00Z&storedquery_id=fmi%3A%3Aobservations%3A%3Aweather%3A%3Amultipointcoverage&timestep=60&version=2.0.0 - + diff --git a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/forecast_multiple_places.xml b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/forecast_multiple_places.xml index 5bcec61e92011..ea1957f5933f1 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/forecast_multiple_places.xml +++ b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/forecast_multiple_places.xml @@ -1,185 +1,187 @@ - - + http://inspire.ec.europa.eu/schemas/omso/3.0 https://inspire.ec.europa.eu/schemas/omso/3.0/SpecialisedObservations.xsd + http://inspire.ec.europa.eu/schemas/ompr/3.0 https://inspire.ec.europa.eu/schemas/ompr/3.0/Processes.xsd + http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.1 http://xml.fmi.fi/schema/om/atmosphericfeatures/1.1/atmosphericfeatures.xsd"> - - - - - 2019-03-27T12:00:00Z - 2019-03-30T18:00:00Z - - - - - 2019-03-27T15:19:43Z - - + + + + + 2021-05-27T12:00:00Z + 2021-05-30T18:00:00Z + + + + + 2021-05-27T15:06:06Z + + - - - - - - - 2019-03-27T12:00:00Z - - - - - - - - - - - - 3041732 - Mariehamn - 3041732 - - Finland - Europe/Mariehamn - Maarianhamina - - - - - NaN - 19.9,61.0973 - NaN - - - Europe/Helsinki + + + + + + + 2021-05-27T12:00:00Z + + + + + + + + + + + + 3041732 + Mariehamn + 3041732 + + Finland + Europe/Mariehamn + Maarianhamina + + + + NaN + 19.9,61.0973 + NaN + + + Europe/Helsinki - - - - - - - - - Mariehamn - 60.09726 19.93481 - - - 19.9,61.0973 - 61.09726 19.90000 - - - - - - - - - - - - 60.09726 19.93481 1553688000 - 60.09726 19.93481 1553709600 - 60.09726 19.93481 1553731200 - 60.09726 19.93481 1553752800 - 60.09726 19.93481 1553774400 - 60.09726 19.93481 1553796000 - 60.09726 19.93481 1553817600 - 60.09726 19.93481 1553839200 - 60.09726 19.93481 1553860800 - 60.09726 19.93481 1553882400 - 60.09726 19.93481 1553904000 - 60.09726 19.93481 1553925600 - 60.09726 19.93481 1553947200 - 60.09726 19.93481 1553968800 - 61.09726 19.90000 1553688000 - 61.09726 19.90000 1553709600 - 61.09726 19.90000 1553731200 - 61.09726 19.90000 1553752800 - 61.09726 19.90000 1553774400 - 61.09726 19.90000 1553796000 - 61.09726 19.90000 1553817600 - 61.09726 19.90000 1553839200 - 61.09726 19.90000 1553860800 - 61.09726 19.90000 1553882400 - 61.09726 19.90000 1553904000 - 61.09726 19.90000 1553925600 - 61.09726 19.90000 1553947200 - 61.09726 19.90000 1553968800 - - - - - - - - 3.84 66.57 - 2.62 87.38 - 2.26 85.77 - 1.22 96.3 - 5.47 75.74 - 5.52 81.7 - 5.42 86.78 - 4.78 87.96 - 8.34 70.86 - 7.15 76.35 - NaN NaN - NaN NaN - NaN NaN - NaN NaN - 1.54 90.18 - 2.91 86.22 - 2.41 89.18 - 2.36 89.43 - 4.22 77.26 - 5.28 78.55 - 4.58 83.36 - 4.0 85.83 - 4.79 80.82 - 5.4 76.92 - NaN NaN - NaN NaN - NaN NaN - NaN NaN - - - - - - Linear - - - - - - - - - - + + + + + + + + Mariehamn + 60.09726 19.93481 + + + 19.9,61.0973 + 61.09726 19.90000 + + + + + + + + + + + + 60.09726 19.93481 1622116800 + 60.09726 19.93481 1622138400 + 60.09726 19.93481 1622160000 + 60.09726 19.93481 1622181600 + 60.09726 19.93481 1622203200 + 60.09726 19.93481 1622224800 + 60.09726 19.93481 1622246400 + 60.09726 19.93481 1622268000 + 60.09726 19.93481 1622289600 + 60.09726 19.93481 1622311200 + 60.09726 19.93481 1622332800 + 60.09726 19.93481 1622354400 + 60.09726 19.93481 1622376000 + 60.09726 19.93481 1622397600 + 61.09726 19.90000 1622116800 + 61.09726 19.90000 1622138400 + 61.09726 19.90000 1622160000 + 61.09726 19.90000 1622181600 + 61.09726 19.90000 1622203200 + 61.09726 19.90000 1622224800 + 61.09726 19.90000 1622246400 + 61.09726 19.90000 1622268000 + 61.09726 19.90000 1622289600 + 61.09726 19.90000 1622311200 + 61.09726 19.90000 1622332800 + 61.09726 19.90000 1622354400 + 61.09726 19.90000 1622376000 + 61.09726 19.90000 1622397600 + + + + + + + + 7.75 93.76 + 7.94 93.24 + 6.72 98.22 + 8.22 93.93 + 11.37 75.78 + 9.69 58.91 + 6.42 80.42 + 9.52 54.11 + 11.04 40.29 + 9.69 46.42 + NaN NaN + NaN NaN + NaN NaN + NaN NaN + 7.46 93.44 + 6.56 95.3 + 6.2 96.15 + 5.15 93.77 + 5.05 93.0 + 5.96 82.1 + 6.2 81.95 + 5.94 81.37 + 5.69 85.41 + 5.47 87.8 + NaN NaN + NaN NaN + NaN NaN + NaN NaN + + + + + + Linear + + + + + + + + + + - - + + diff --git a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_empty.xml b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_empty.xml index 04753086c1012..ffcc4329e5d12 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_empty.xml +++ b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_empty.xml @@ -1,22 +1,31 @@ - + http://inspire.ec.europa.eu/schemas/ompr/3.0 https://inspire.ec.europa.eu/schemas/ompr/3.0/Processes.xsd + http://inspire.ec.europa.eu/schemas/omso/3.0 https://inspire.ec.europa.eu/schemas/omso/3.0/SpecialisedObservations.xsd + http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.1 http://xml.fmi.fi/schema/om/atmosphericfeatures/1.1/atmosphericfeatures.xsd"> - + \ No newline at end of file diff --git a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_multiple_places.xml b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_multiple_places.xml index ef343102db084..457481571549d 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_multiple_places.xml +++ b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_multiple_places.xml @@ -1,225 +1,221 @@ - - - - - - - - 2019-03-10T10:10:00Z - 2019-03-10T20:10:00Z - - - - - 2019-03-10T20:10:00Z - - - - - - - - - atmosphere - - - - - - - - - - - - - 100683 - Porvoo Kilpilahti satama - -16777356 - 2994 - - - - Porvoo - - - - - - 101023 - Porvoo Emäsalo - -16000110 - 2991 - - - - Porvoo - - - - - - 101028 - Porvoo Harabacka - -16000142 - 2759 - - - - Porvoo - - - - - - - - - - Porvoo Kilpilahti satama - 60.30373 25.54916 - - - - - Porvoo Emäsalo - 60.20382 25.62546 - - - - - Porvoo Harabacka - 60.39172 25.60730 - - - - - - - - - - - - - 60.30373 25.54916 1552215600 - 60.30373 25.54916 1552219200 - 60.30373 25.54916 1552222800 - 60.30373 25.54916 1552226400 - 60.30373 25.54916 1552230000 - 60.30373 25.54916 1552233600 - 60.30373 25.54916 1552237200 - 60.30373 25.54916 1552240800 - 60.30373 25.54916 1552244400 - 60.30373 25.54916 1552248000 - 60.20382 25.62546 1552215600 - 60.20382 25.62546 1552219200 - 60.20382 25.62546 1552222800 - 60.20382 25.62546 1552226400 - 60.20382 25.62546 1552230000 - 60.20382 25.62546 1552233600 - 60.20382 25.62546 1552237200 - 60.20382 25.62546 1552240800 - 60.20382 25.62546 1552244400 - 60.20382 25.62546 1552248000 - 60.39172 25.60730 1552215600 - 60.39172 25.60730 1552219200 - 60.39172 25.60730 1552222800 - 60.39172 25.60730 1552226400 - 60.39172 25.60730 1552230000 - 60.39172 25.60730 1552233600 - 60.39172 25.60730 1552237200 - 60.39172 25.60730 1552240800 - 60.39172 25.60730 1552244400 - 60.39172 25.60730 1552248000 - - - - - - - - -0.5 73.0 299.0 5.3 8.2 NaN - -0.6 65.0 293.0 7.0 9.1 NaN - -0.9 60.0 300.0 6.2 9.8 NaN - -1.2 59.0 288.0 6.3 8.9 NaN - -1.2 57.0 256.0 4.6 7.1 NaN - -1.6 64.0 232.0 2.4 5.2 NaN - -1.9 66.0 239.0 1.9 3.2 NaN - -2.3 65.0 249.0 3.1 5.0 NaN - -2.9 71.0 280.0 4.3 5.7 NaN - -3.3 77.0 246.0 3.4 5.6 NaN - -0.4 77.0 312.0 8.0 10.0 985.9 - 0.0 70.0 286.0 7.5 9.0 986.5 - 0.1 61.0 295.0 8.6 10.5 987.0 - -1.0 64.0 282.0 8.4 10.5 987.6 - -1.2 65.0 271.0 6.6 8.7 988.1 - -1.3 61.0 262.0 5.0 6.7 988.2 - -1.2 65.0 243.0 8.2 9.6 988.1 - -1.5 69.0 252.0 6.1 7.6 987.9 - -1.7 71.0 262.0 7.3 8.6 988.0 - -2.4 77.0 276.0 6.0 7.5 988.2 - -0.6 74.0 317.0 3.9 7.1 985.9 - -0.9 64.0 290.0 4.2 6.8 986.4 - -1.0 58.0 296.0 5.3 9.1 987.0 - -1.7 66.0 301.0 3.5 6.6 987.6 - -1.9 64.0 269.0 2.6 4.2 988.0 - -2.8 71.0 231.0 1.1 2.3 988.1 - -3.4 78.0 229.0 1.1 1.6 988.1 - -3.8 79.0 229.0 1.8 2.7 987.8 - -4.1 81.0 253.0 2.0 3.2 988.0 - -4.7 86.0 224.0 1.9 3.1 988.2 - - - - - - Linear - - - - - - - - - - - - - - - - - + http://inspire.ec.europa.eu/schemas/ompr/3.0 https://inspire.ec.europa.eu/schemas/ompr/3.0/Processes.xsd + http://inspire.ec.europa.eu/schemas/omso/3.0 https://inspire.ec.europa.eu/schemas/omso/3.0/SpecialisedObservations.xsd + http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.1 http://xml.fmi.fi/schema/om/atmosphericfeatures/1.1/atmosphericfeatures.xsd"> + + + + + + + 2019-03-10T10:10:00Z + 2019-03-10T20:10:00Z + + + + + 2019-03-10T20:10:00Z + + + + + + + + + atmosphere + + + + + + + + + + + + + 100683 + Porvoo Kilpilahti satama + -16777356 + 2994 + + + + Porvoo + + + + + 101023 + Porvoo Emäsalo + -16000110 + 2991 + + + + Porvoo + + + + + 101028 + Porvoo Harabacka + -16000142 + 2759 + + + + Porvoo + + + + + + + + + Porvoo Kilpilahti satama + 60.30373 25.54916 + + + + + Porvoo Emäsalo + 60.20382 25.62546 + + + + + Porvoo Harabacka + 60.39172 25.60730 + + + + + + + + + + + + + 60.30373 25.54916 1552215600 + 60.30373 25.54916 1552219200 + 60.30373 25.54916 1552222800 + 60.30373 25.54916 1552226400 + 60.30373 25.54916 1552230000 + 60.30373 25.54916 1552233600 + 60.30373 25.54916 1552237200 + 60.30373 25.54916 1552240800 + 60.30373 25.54916 1552244400 + 60.30373 25.54916 1552248000 + 60.20382 25.62546 1552215600 + 60.20382 25.62546 1552219200 + 60.20382 25.62546 1552222800 + 60.20382 25.62546 1552226400 + 60.20382 25.62546 1552230000 + 60.20382 25.62546 1552233600 + 60.20382 25.62546 1552237200 + 60.20382 25.62546 1552240800 + 60.20382 25.62546 1552244400 + 60.20382 25.62546 1552248000 + 60.39172 25.60730 1552215600 + 60.39172 25.60730 1552219200 + 60.39172 25.60730 1552222800 + 60.39172 25.60730 1552226400 + 60.39172 25.60730 1552230000 + 60.39172 25.60730 1552233600 + 60.39172 25.60730 1552237200 + 60.39172 25.60730 1552240800 + 60.39172 25.60730 1552244400 + 60.39172 25.60730 1552248000 + + + + + + + + -0.5 73.0 299.0 5.3 8.2 NaN + -0.6 65.0 293.0 7.0 9.1 NaN + -0.9 60.0 300.0 6.2 9.8 NaN + -1.2 59.0 288.0 6.3 8.9 NaN + -1.2 57.0 256.0 4.6 7.1 NaN + -1.6 64.0 232.0 2.4 5.2 NaN + -1.9 66.0 239.0 1.9 3.2 NaN + -2.3 65.0 249.0 3.1 5.0 NaN + -2.9 71.0 280.0 4.3 5.7 NaN + -3.3 77.0 246.0 3.4 5.6 NaN + -0.4 77.0 312.0 8.0 10.0 985.9 + 0.0 70.0 286.0 7.5 9.0 986.5 + 0.1 61.0 295.0 8.6 10.5 987.0 + -1.0 64.0 282.0 8.4 10.5 987.6 + -1.2 65.0 271.0 6.6 8.7 988.1 + -1.3 61.0 262.0 5.0 6.7 988.2 + -1.2 65.0 243.0 8.2 9.6 988.1 + -1.5 69.0 252.0 6.1 7.6 987.9 + -1.7 71.0 262.0 7.3 8.6 988.0 + -2.4 77.0 276.0 6.0 7.5 988.2 + -0.6 74.0 317.0 3.9 7.1 985.9 + -0.9 64.0 290.0 4.2 6.8 986.4 + -1.0 58.0 296.0 5.3 9.1 987.0 + -1.7 66.0 301.0 3.5 6.6 987.6 + -1.9 64.0 269.0 2.6 4.2 988.0 + -2.8 71.0 231.0 1.1 2.3 988.1 + -3.4 78.0 229.0 1.1 1.6 988.1 + -3.8 79.0 229.0 1.8 2.7 987.8 + -4.1 81.0 253.0 2.0 3.2 988.0 + -4.7 86.0 224.0 1.9 3.1 988.2 + + + + + + Linear + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_single_place.xml b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_single_place.xml index 45ad13307f79d..e5cb977423e52 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_single_place.xml +++ b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/observations_single_place.xml @@ -1,146 +1,144 @@ - - + + http://inspire.ec.europa.eu/schemas/ompr/3.0 https://inspire.ec.europa.eu/schemas/ompr/3.0/Processes.xsd + http://inspire.ec.europa.eu/schemas/omso/3.0 https://inspire.ec.europa.eu/schemas/omso/3.0/SpecialisedObservations.xsd + http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.1 http://xml.fmi.fi/schema/om/atmosphericfeatures/1.1/atmosphericfeatures.xsd"> - - + + - - - 2019-03-10T10:10:00Z - 2019-03-10T20:10:00Z - - - - - 2019-03-10T20:10:00Z - - + + + 2019-03-10T10:10:00Z + 2019-03-10T20:10:00Z + + + + + 2019-03-10T20:10:00Z + + - - - - - - atmosphere - - - + + + + + + atmosphere + + + - - - + + + - - - - - 101023 - Porvoo Emäsalo - -16000110 - 2991 - + + + + + 101023 + Porvoo Emäsalo + -16000110 + 2991 + - Porvoo + Porvoo - - - - - - - - - Porvoo Emäsalo - 60.20382 25.62546 - - - - - - + + + + + + + + Porvoo Emäsalo + 60.20382 25.62546 + + + + + + - - - - - - 60.20382 25.62546 1552215600 - 60.20382 25.62546 1552219200 - 60.20382 25.62546 1552222800 - 60.20382 25.62546 1552226400 - 60.20382 25.62546 1552230000 - 60.20382 25.62546 1552233600 - 60.20382 25.62546 1552237200 - 60.20382 25.62546 1552240800 - 60.20382 25.62546 1552244400 - 60.20382 25.62546 1552248000 - - - - - - - - -0.4 77.0 312.0 8.0 10.0 985.9 - 0.0 70.0 286.0 7.5 9.0 986.5 - 0.1 61.0 295.0 8.6 10.5 987.0 - -1.0 64.0 282.0 8.4 10.5 987.6 - -1.2 65.0 271.0 6.6 8.7 988.1 - -1.3 61.0 262.0 5.0 6.7 988.2 - -1.2 65.0 243.0 8.2 9.6 988.1 - -1.5 69.0 252.0 6.1 7.6 987.9 - -1.7 71.0 262.0 7.3 8.6 988.0 - -2.4 77.0 276.0 6.0 7.5 988.2 - - - - - - Linear - - - - - - - - - - - - - - + + + + + + 60.20382 25.62546 1552215600 + 60.20382 25.62546 1552219200 + 60.20382 25.62546 1552222800 + 60.20382 25.62546 1552226400 + 60.20382 25.62546 1552230000 + 60.20382 25.62546 1552233600 + 60.20382 25.62546 1552237200 + 60.20382 25.62546 1552240800 + 60.20382 25.62546 1552244400 + 60.20382 25.62546 1552248000 + + + + + + + + -0.4 77.0 312.0 8.0 10.0 985.9 + 0.0 70.0 286.0 7.5 9.0 986.5 + 0.1 61.0 295.0 8.6 10.5 987.0 + -1.0 64.0 282.0 8.4 10.5 987.6 + -1.2 65.0 271.0 6.6 8.7 988.1 + -1.3 61.0 262.0 5.0 6.7 988.2 + -1.2 65.0 243.0 8.2 9.6 988.1 + -1.5 69.0 252.0 6.1 7.6 987.9 + -1.7 71.0 262.0 7.3 8.6 988.0 + -2.4 77.0 276.0 6.0 7.5 988.2 + + + + + + Linear + + + + + + + + + + + + + + - - + + diff --git a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/stations.xml b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/stations.xml index 0168746f09b69..f73857a69d693 100644 --- a/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/stations.xml +++ b/bundles/org.openhab.binding.fmiweather/src/test/resources/org/openhab/binding/fmiweather/stations.xml @@ -1,140 +1,133 @@ - - - - - 100683 - Porvoo Kilpilahti satama - -16777356 - 02994 - Porvoo - Suomi - - - 100683 - http://xml.fmi.fi/namespace/identifier/station/inspire - - - Porvoo Kilpilahti satama - - - - 60.303725 25.549164 - - - - false - - - - - 2014-06-19T00:00:00Z - - - - - - - - - - - 100908 - Parainen Utö - -16000054 - 02981 - Parainen - Suomi - - - 100908 - http://xml.fmi.fi/namespace/identifier/station/inspire - - - Parainen Utö - - - - 59.779094 21.374788 - - - - false - - - - - 1881-02-01T00:00:00Z - - - - - - - - - - - - - - - 100909 - Lemland Nyhamn - -16000086 - 02980 - Lemland - Suomi - - - 100909 - http://xml.fmi.fi/namespace/identifier/station/inspire - - - Lemland Nyhamn - - - - 59.959108 19.953736 - - - - false - - - - - 1958-10-01T00:00:00Z - - - - - - - - - + + + + + 100683 + Porvoo Kilpilahti satama + -16777356 + 02994 + Porvoo + Suomi + + + 100683 + http://xml.fmi.fi/namespace/identifier/station/inspire + + + Porvoo Kilpilahti satama + + + + 60.303725 25.549164 + + + + false + + + + + 2014-06-19T00:00:00Z + + + + + + + + + + + 100908 + Parainen Utö + -16000054 + 02981 + Parainen + Suomi + + + 100908 + http://xml.fmi.fi/namespace/identifier/station/inspire + + + Parainen Utö + + + + 59.779094 21.374788 + + + + false + + + + + 1881-02-01T00:00:00Z + + + + + + + + + + + + + + + + 100909 + Lemland Nyhamn + -16000086 + 02980 + Lemland + Suomi + + + 100909 + http://xml.fmi.fi/namespace/identifier/station/inspire + + + Lemland Nyhamn + + + + 59.959194 19.953667 + + + + false + + + + + 1958-09-16T00:00:00Z + + + + + + + + + + diff --git a/bundles/org.openhab.binding.gpio/README.md b/bundles/org.openhab.binding.gpio/README.md index 622cf483582bd..861fe036a5a70 100644 --- a/bundles/org.openhab.binding.gpio/README.md +++ b/bundles/org.openhab.binding.gpio/README.md @@ -28,13 +28,17 @@ Note: if you are setting this up on a Raspberry Pi without `raspi-config` you ca sudo mkdir -p /etc/systemd/system/pigpiod.service.d/ sudo nano /etc/systemd/system/pigpiod.service.d/public.conf ``` - [Service] - ExecStart= - ExecStart=/usr/bin/pigpiod + + [Service] + ExecStart= + ExecStart=/usr/bin/pigpiod + ``` sudo systemctl daemon-reload ``` -Now that Remote GPIO is enabled, get the daemon going: + +Now that Remote GPIO is enabled, get the daemon going (even if installed with apt-get): + ``` sudo systemctl enable pigpiod sudo systemctl start pigpiod @@ -42,6 +46,8 @@ sudo systemctl start pigpiod In openHAB, set `host` to the address of the pi and the `port` to the port of pigpio (default: 8888). +Note: If you are running Pigpio on same host as openHAB, then set host to **::1**. + ## Channels ### Pigpio Remote @@ -56,6 +62,7 @@ In openHAB, set `host` to the address of the pi and the `port` to the port of pi Set the number of the pin in `gpioId`. If you want to invert the value, set `invert` to true. To prevent incorrect change events, you can adjust the `debouncingTime`. +Using `pullupdown` you can enable pull up or pull down resistor (OFF = Off, DOWN = Pull Down, UP = Pull Up). ### GPIO digital output channel @@ -78,7 +85,7 @@ Thing gpio:pigpio-remote:sample-pi-1 "Sample-Pi 1" [host="192.168.2.36", port=88 Thing gpio:pigpio-remote:sample-pi-2 "Sample-Pi 2" [host="192.168.2.37", port=8888] { Channels: Type pigpio-digital-input : sample-input-3 [ gpioId=16, debouncingTime=20] - Type pigpio-digital-input : sample-input-4 [ gpioId=17, invert=true, debouncingTime=5] + Type pigpio-digital-input : sample-input-4 [ gpioId=17, invert=true, debouncingTime=5, pullupdown="UP"] Type pigpio-digital-output : sample-output-2 [ gpioId=4, invert=true] } ``` diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/GPIOBindingConstants.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/GPIOBindingConstants.java index 8d6ea65787bb1..7a56a285f14d0 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/GPIOBindingConstants.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/GPIOBindingConstants.java @@ -21,6 +21,7 @@ * used across the whole binding. * * @author Nils Bauer - Initial contribution + * @author Martin Dagarin - Pull Up/Down GPIO pin */ @NonNullByDefault public class GPIOBindingConstants { @@ -41,6 +42,12 @@ public class GPIOBindingConstants { public static final String INVERT = "invert"; public static final String DEBOUNCING_TIME = "debouncing_time"; public static final String STRICT_DEBOUNCING = "debouncing_strict"; + public static final String PULLUPDOWN_RESISTOR = "pullupdown"; + + // Pull Up/Down modes + public static final String PUD_OFF = "OFF"; + public static final String PUD_DOWN = "DOWN"; + public static final String PUD_UP = "UP"; // GPIO config properties public static final String GPIO_ID = "gpioId"; diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/InvalidPullUpDownException.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/InvalidPullUpDownException.java new file mode 100644 index 0000000000000..879c118f85987 --- /dev/null +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/InvalidPullUpDownException.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.gpio.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Is thrown when invalid GPIO pin Pull Up/Down resistor configuration is set + * + * @author Martin Dagarin - Initial contribution + */ +@NonNullByDefault +public class InvalidPullUpDownException extends Exception { + private static final long serialVersionUID = -1281107134439928767L; +} diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOInputConfiguration.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOInputConfiguration.java index ef1a91153811c..b4df29b724c56 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOInputConfiguration.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/configuration/GPIOInputConfiguration.java @@ -18,6 +18,7 @@ * The {@link GPIOInputConfiguration} class contains fields mapping thing configuration parameters. * * @author Nils Bauer - Initial contribution + * @author Martin Dagarin - Pull Up/Down GPIO pin */ @NonNullByDefault public class GPIOInputConfiguration extends GPIOConfiguration { @@ -25,4 +26,10 @@ public class GPIOInputConfiguration extends GPIOConfiguration { * Time in ms to double check if value hasn't changed */ public int debouncingTime = 10; + + /** + * Setup a pullup resistor on the GPIO pin + * OFF = PI_PUD_OFF, DOWN = PI_PUD_DOWN, UP = PI_PUD_UP + */ + public String pullupdown = "OFF"; } diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalInputHandler.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalInputHandler.java index 06abc8fbabb0a..17db1a4c81876 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalInputHandler.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalInputHandler.java @@ -18,6 +18,8 @@ import java.util.function.Consumer; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.gpio.internal.GPIOBindingConstants; +import org.openhab.binding.gpio.internal.InvalidPullUpDownException; import org.openhab.binding.gpio.internal.NoGpioIdException; import org.openhab.binding.gpio.internal.configuration.GPIOInputConfiguration; import org.openhab.core.library.types.OnOffType; @@ -36,6 +38,7 @@ * * @author Nils Bauer - Initial contribution * @author Jan N. Klug - Channel redesign + * @author Martin Dagarin - Pull Up/Down GPIO pin */ @NonNullByDefault public class PigpioDigitalInputHandler implements ChannelHandler { @@ -49,19 +52,30 @@ public class PigpioDigitalInputHandler implements ChannelHandler { public PigpioDigitalInputHandler(GPIOInputConfiguration configuration, JPigpio jPigpio, ScheduledExecutorService scheduler, Consumer updateStatus) - throws PigpioException, NoGpioIdException { + throws PigpioException, InvalidPullUpDownException, NoGpioIdException { this.configuration = configuration; this.updateStatus = updateStatus; Integer gpioId = configuration.gpioId; if (gpioId == null) { throw new NoGpioIdException(); } - gpio = new GPIO(jPigpio, gpioId, 1); + Integer pullupdown = JPigpio.PI_PUD_OFF; + String pullupdownStr = configuration.pullupdown.toUpperCase(); + if (pullupdownStr.equals(GPIOBindingConstants.PUD_DOWN)) { + pullupdown = JPigpio.PI_PUD_DOWN; + } else if (pullupdownStr.equals(GPIOBindingConstants.PUD_UP)) { + pullupdown = JPigpio.PI_PUD_UP; + } else { + if (!pullupdownStr.equals(GPIOBindingConstants.PUD_OFF)) + throw new InvalidPullUpDownException(); + } + gpio = new GPIO(jPigpio, gpioId, JPigpio.PI_INPUT); jPigpio.gpioSetAlertFunc(gpio.getPin(), (gpio, level, tick) -> { lastChanged = new Date(); Date thisChange = new Date(); scheduler.schedule(() -> afterDebounce(thisChange), configuration.debouncingTime, TimeUnit.MILLISECONDS); }); + jPigpio.gpioSetPullUpDown(gpio.getPin(), pullupdown); } private void afterDebounce(Date thisChange) { diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalOutputHandler.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalOutputHandler.java index d0ef7993c35fd..97c5d3d1e8708 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalOutputHandler.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioDigitalOutputHandler.java @@ -62,7 +62,7 @@ public PigpioDigitalOutputHandler(GPIOOutputConfiguration configuration, JPigpio if (gpioId == null) { throw new NoGpioIdException(); } - this.gpio = new GPIO(jPigpio, gpioId, 0); + this.gpio = new GPIO(jPigpio, gpioId, JPigpio.PI_OUTPUT); } @Override diff --git a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioRemoteHandler.java b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioRemoteHandler.java index 993b66dc33589..46bdb4d0f4500 100644 --- a/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioRemoteHandler.java +++ b/bundles/org.openhab.binding.gpio/src/main/java/org/openhab/binding/gpio/internal/handler/PigpioRemoteHandler.java @@ -18,6 +18,7 @@ import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.gpio.internal.InvalidPullUpDownException; import org.openhab.binding.gpio.internal.NoGpioIdException; import org.openhab.binding.gpio.internal.configuration.GPIOInputConfiguration; import org.openhab.binding.gpio.internal.configuration.GPIOOutputConfiguration; @@ -105,6 +106,8 @@ public void initialize() { } } catch (PigpioException e) { logger.warn("Failed to initialize {}: {}", channelUID, e.getMessage()); + } catch (InvalidPullUpDownException e) { + logger.warn("Failed to initialize {}: Invalid Pull Up/Down resistor configuration", channelUID); } catch (NoGpioIdException e) { logger.warn("Failed to initialize {}: GpioId is not set", channelUID); } diff --git a/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/thing/pigpio-remote.xml b/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/thing/pigpio-remote.xml index 3dbda62f10642..f71d13ca66653 100644 --- a/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/thing/pigpio-remote.xml +++ b/bundles/org.openhab.binding.gpio/src/main/resources/OH-INF/thing/pigpio-remote.xml @@ -50,6 +50,17 @@ 10 true + + + Configure Pull Up/Down Resistor of GPIO pin + + + + + + true + OFF + diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBackyardHandler.java b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBackyardHandler.java index 0419770ea47be..20d6c305ff10b 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBackyardHandler.java +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBackyardHandler.java @@ -101,7 +101,7 @@ public boolean getAlarmList(String systemID) throws HaywardException { .evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse) .get(0); - if (!(status.equals("0"))) { + if (!("0".equals(status))) { logger.trace("Hayward getAlarm XML response: {}", xmlResponse); return false; } diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBridgeHandler.java b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBridgeHandler.java index 8e3d1d9919ad6..13792f3e9892f 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBridgeHandler.java +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardBridgeHandler.java @@ -186,7 +186,7 @@ public synchronized boolean login() throws HaywardException, InterruptedExceptio status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0); - if (!(status.equals("0"))) { + if (!("0".equals(status))) { logger.debug("Hayward Connection thing: Login XML response: {}", xmlResponse); return false; } @@ -234,7 +234,7 @@ public synchronized boolean getSiteList() throws HaywardException, InterruptedEx status = evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlResponse).get(0); - if (!(status.equals("0"))) { + if (!("0".equals(status))) { logger.debug("Hayward Connection thing: getSiteList XML response: {}", xmlResponse); return false; } diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardChlorinatorHandler.java b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardChlorinatorHandler.java index 0d1e12f7aa32e..8743f10728332 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardChlorinatorHandler.java +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardChlorinatorHandler.java @@ -128,7 +128,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { try { switch (channelUID.getId()) { case HaywardBindingConstants.CHANNEL_CHLORINATOR_ENABLE: - if (cmdString.equals("1")) { + if ("1".equals(cmdString)) { chlorCfgState = "3"; chlorTimedPercent = this.chlorTimedPercent; } else { @@ -168,7 +168,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { String status = bridgehandler.evaluateXPath("//Parameter[@name='Status']/text()", xmlResponse) .get(0); - if (!(status.equals("0"))) { + if (!("0".equals(status))) { logger.debug("haywardCommand XML response: {}", xmlResponse); return; } diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardColorLogicHandler.java b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardColorLogicHandler.java index bb42b06eb97c1..ed98a1be63fa0 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardColorLogicHandler.java +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardColorLogicHandler.java @@ -135,7 +135,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { String status = bridgehandler.evaluateXPath("//Parameter[@name='Status']/text()", xmlResponse) .get(0); - if (!(status.equals("0"))) { + if (!("0".equals(status))) { logger.debug("haywardCommand XML response: {}", xmlResponse); return; } diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardFilterHandler.java b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardFilterHandler.java index b39740cd8d38d..edf7a96d33b05 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardFilterHandler.java +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardFilterHandler.java @@ -140,7 +140,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { String status = bridgehandler.evaluateXPath("//Parameter[@name='Status']/text()", xmlResponse) .get(0); - if (!(status.equals("0"))) { + if (!("0".equals(status))) { logger.debug("haywardCommand XML response: {}", xmlResponse); return; } diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardRelayHandler.java b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardRelayHandler.java index f890cd8e708c2..c002d5acc12b0 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardRelayHandler.java +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardRelayHandler.java @@ -104,7 +104,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { String status = bridgehandler.evaluateXPath("//Parameter[@name='Status']/text()", xmlResponse) .get(0); - if (!(status.equals("0"))) { + if (!("0".equals(status))) { logger.debug("haywardCommand XML response: {}", xmlResponse); return; } diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardVirtualHeaterHandler.java b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardVirtualHeaterHandler.java index 6ee8246a85628..b06a7e96b290a 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardVirtualHeaterHandler.java +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/java/org/openhab/binding/haywardomnilogic/internal/handler/HaywardVirtualHeaterHandler.java @@ -131,7 +131,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { String status = bridgehandler.evaluateXPath("//Parameter[@name='Status']/text()", xmlResponse) .get(0); - if (!(status.equals("0"))) { + if (!("0".equals(status))) { logger.debug("haywardCommand XML response: {}", xmlResponse); return; } diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/backyard.xml b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/backyard.xml index c947bc3077633..16983ad5157ae 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/backyard.xml +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/backyard.xml @@ -33,7 +33,7 @@ Number:Temperature Air Temp - + diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/bow.xml b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/bow.xml index aa594741bb4c3..ca07c5cf92b3b 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/bow.xml +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/bow.xml @@ -34,7 +34,7 @@ Number:Temperature Water Temp - + diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/chlorinator.xml b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/chlorinator.xml index 4b77e92ca29a3..a0e6bd8849ad0 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/chlorinator.xml +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/chlorinator.xml @@ -47,7 +47,7 @@ Number:Dimensionless Current salt output setting for the chlorinator (%). - + @@ -84,20 +84,20 @@ Number:Dimensionless Average Salt Level - + Number:Dimensionless Instant Salt Level - + Number Status - + diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/filter.xml b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/filter.xml index 00529184bb7f9..886904432da7f 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/filter.xml +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/filter.xml @@ -48,7 +48,7 @@ Number:Dimensionless Filter Speed in % - + @@ -77,7 +77,7 @@ Number:Dimensionless Last Speed - + diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/pump.xml b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/pump.xml index 16f1a094b6e20..d9e43f45e446e 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/pump.xml +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/pump.xml @@ -31,7 +31,7 @@ Number:Dimensionless Pump Speed - + diff --git a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/virtualHeater.xml b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/virtualHeater.xml index 89d7d410c17ed..65c502e39d870 100644 --- a/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/virtualHeater.xml +++ b/bundles/org.openhab.binding.haywardomnilogic/src/main/resources/OH-INF/thing/virtualHeater.xml @@ -28,7 +28,7 @@ Current Setpoint Temperature - + diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java index 67ce71ab699b0..7ecdbec37fa78 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/HomeConnectBindingConstants.java @@ -76,6 +76,7 @@ public class HomeConnectBindingConstants { public static final String EVENT_REMOTE_CONTROL_START_ALLOWED = STATUS_REMOTE_CONTROL_START_ALLOWED; public static final String EVENT_REMOTE_CONTROL_ACTIVE = STATUS_REMOTE_CONTROL_ACTIVE; public static final String EVENT_LOCAL_CONTROL_ACTIVE = STATUS_LOCAL_CONTROL_ACTIVE; + public static final String EVENT_FINISH_IN_RELATIVE = "BSH.Common.Option.FinishInRelative"; public static final String EVENT_REMAINING_PROGRAM_TIME = "BSH.Common.Option.RemainingProgramTime"; public static final String EVENT_PROGRAM_PROGRESS = "BSH.Common.Option.ProgramProgress"; public static final String EVENT_SETPOINT_TEMPERATURE = "Cooking.Oven.Option.SetpointTemperature"; @@ -162,6 +163,7 @@ public class HomeConnectBindingConstants { public static final String STATE_EVENT_PRESENT_STATE_OFF = "BSH.Common.EnumType.EventPresentState.Off"; // List of program options + public static final String OPTION_FINISH_IN_RELATIVE = "BSH.Common.Option.FinishInRelative"; public static final String OPTION_REMAINING_PROGRAM_TIME = "BSH.Common.Option.RemainingProgramTime"; public static final String OPTION_PROGRAM_PROGRESS = "BSH.Common.Option.ProgramProgress"; public static final String OPTION_ELAPSED_PROGRAM_TIME = "BSH.Common.Option.ElapsedProgramTime"; diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java index 4ea338e7b7035..feba2087b4fa0 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectApiClient.java @@ -12,13 +12,9 @@ */ package org.openhab.binding.homeconnect.internal.client; -import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.openhab.binding.homeconnect.internal.HomeConnectBindingConstants.*; -import static org.openhab.binding.homeconnect.internal.client.HttpHelper.formatJsonBody; -import static org.openhab.binding.homeconnect.internal.client.HttpHelper.getAuthorizationHeader; -import static org.openhab.binding.homeconnect.internal.client.HttpHelper.parseString; -import static org.openhab.binding.homeconnect.internal.client.HttpHelper.sendRequest; +import static org.openhab.binding.homeconnect.internal.client.HttpHelper.*; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -83,7 +79,7 @@ public class HomeConnectApiClient { private final Logger logger = LoggerFactory.getLogger(HomeConnectApiClient.class); private final HttpClient client; private final String apiUrl; - private final Map> availableProgramOptionsCache; + private final Map> programsCache; private final OAuthClientService oAuthClientService; private final CircularQueue communicationQueue; private final ApiBridgeConfiguration apiBridgeConfiguration; @@ -94,7 +90,7 @@ public HomeConnectApiClient(HttpClient httpClient, OAuthClientService oAuthClien this.oAuthClientService = oAuthClientService; this.apiBridgeConfiguration = apiBridgeConfiguration; - availableProgramOptionsCache = new ConcurrentHashMap<>(); + programsCache = new ConcurrentHashMap<>(); apiUrl = simulated ? API_SIMULATOR_BASE_URL : API_BASE_URL; communicationQueue = new CircularQueue<>(COMMUNICATION_QUEUE_SIZE); if (apiRequestHistory != null) { @@ -614,7 +610,16 @@ public void stopProgram(String haId) public List getPrograms(String haId) throws CommunicationException, AuthorizationException, ApplianceOfflineException { - return getAvailablePrograms(haId, BASE_PATH + haId + "/programs"); + List programs; + if (programsCache.containsKey(haId)) { + logger.debug("Returning cached programs for '{}'.", haId); + programs = programsCache.get(haId); + programs = programs != null ? programs : Collections.emptyList(); + } else { + programs = getAvailablePrograms(haId, BASE_PATH + haId + "/programs"); + programsCache.put(haId, programs); + } + return programs; } public List getAvailablePrograms(String haId) @@ -624,23 +629,23 @@ public List getAvailablePrograms(String haId) public List getProgramOptions(String haId, String programKey) throws CommunicationException, AuthorizationException, ApplianceOfflineException { - if (availableProgramOptionsCache.containsKey(programKey)) { - logger.debug("Returning cached options for '{}'.", programKey); - List availableProgramOptions = availableProgramOptionsCache.get(programKey); - return availableProgramOptions != null ? availableProgramOptions : Collections.emptyList(); - } - Request request = createRequest(HttpMethod.GET, BASE_PATH + haId + "/programs/available/" + programKey); try { ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); - checkResponseCode(HttpStatus.OK_200, request, response, haId, null); + checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null); String responseBody = response.getContentAsString(); trackAndLogApiRequest(haId, request, null, response, responseBody); - List availableProgramOptions = mapToAvailableProgramOption(responseBody, haId); - availableProgramOptionsCache.put(programKey, availableProgramOptions); - return availableProgramOptions; + // Code 404 accepted only if the returned error is "SDK.Error.UnsupportedProgram" + if (response.getStatus() == HttpStatus.NOT_FOUND_404 + && (responseBody == null || !responseBody.contains("SDK.Error.UnsupportedProgram"))) { + throw new CommunicationException(HttpStatus.NOT_FOUND_404, response.getReason(), + responseBody == null ? "" : responseBody); + } + + return response.getStatus() == HttpStatus.OK_200 ? mapToAvailableProgramOption(responseBody, haId) + : List.of(); } catch (InterruptedException | TimeoutException | ExecutionException e) { logger.warn("Failed to get program options! haId={}, programKey={}, error={}", haId, programKey, e.getMessage()); @@ -728,7 +733,7 @@ public String putRaw(String haId, String path, String requestBodyPayload) Request request = createRequest(HttpMethod.GET, path); try { ContentResponse response = sendRequest(request, apiBridgeConfiguration.getClientId()); - checkResponseCode(asList(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null); + checkResponseCode(List.of(HttpStatus.OK_200, HttpStatus.NOT_FOUND_404), request, response, haId, null); String responseBody = response.getContentAsString(); trackAndLogApiRequest(haId, request, null, response, responseBody); diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectEventSourceClient.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectEventSourceClient.java index ab0faf124d5a9..94fcb5e587b41 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectEventSourceClient.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectEventSourceClient.java @@ -95,11 +95,11 @@ public synchronized void registerEventListener(final String haId, final HomeConn if (!eventSourceConnections.containsKey(eventListener)) { logger.debug("Create new event source listener for '{}'.", haId); - Client client = clientBuilder.readTimeout(SSE_REQUEST_READ_TIMEOUT, TimeUnit.SECONDS).register( - new HomeConnectStreamingRequestFilter(HttpHelper.getAuthorizationHeader(oAuthClientService))) - .build(); - SseEventSource eventSource = eventSourceFactory - .newSource(client.target(apiUrl + "/api/homeappliances/" + haId + "/events")); + String target = apiUrl + "/api/homeappliances/" + haId + "/events"; + + Client client = createClient(target); + + SseEventSource eventSource = eventSourceFactory.newSource(client.target(target)); HomeConnectEventSourceListener eventSourceListener = new HomeConnectEventSourceListener(haId, eventListener, this, scheduler, eventQueue); eventSource.register(eventSourceListener::onEvent, eventSourceListener::onError, @@ -149,9 +149,11 @@ public synchronized void unregisterEventListener(HomeConnectEventListener eventL } private void closeEventSource(SseEventSource eventSource, boolean immediate, boolean completed) { - if (eventSource.isOpen() && !completed) { - logger.debug("Close event source (immediate = {})", immediate); - eventSource.close(immediate ? 0 : 10, TimeUnit.SECONDS); + var open = eventSource.isOpen(); + logger.debug("Closing event source. open={}, completed={}, immediate={}", open, completed, immediate); + if (open && !completed) { + eventSource.close(immediate ? 0 : 5, TimeUnit.SECONDS); + logger.debug("Event source closed."); } HomeConnectEventSourceListener eventSourceListener = eventSourceListeners.get(eventSource); if (eventSourceListener != null) { @@ -159,6 +161,26 @@ private void closeEventSource(SseEventSource eventSource, boolean immediate, boo } } + private Client createClient(String target) throws CommunicationException, AuthorizationException { + boolean filterRegistered = clientBuilder.getConfiguration() + .isRegistered(HomeConnectStreamingRequestFilter.class); + + Client client; + HomeConnectStreamingRequestFilter filter; + if (filterRegistered) { + filter = clientBuilder.getConfiguration().getInstances().stream() + .filter(instance -> instance instanceof HomeConnectStreamingRequestFilter) + .map(instance -> (HomeConnectStreamingRequestFilter) instance).findAny().orElseThrow(); + client = clientBuilder.readTimeout(SSE_REQUEST_READ_TIMEOUT, TimeUnit.SECONDS).build(); + } else { + filter = new HomeConnectStreamingRequestFilter(); + client = clientBuilder.readTimeout(SSE_REQUEST_READ_TIMEOUT, TimeUnit.SECONDS).register(filter).build(); + } + filter.setAuthorizationHeader(target, HttpHelper.getAuthorizationHeader(oAuthClientService)); + + return client; + } + /** * Connection count. * diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectEventSourceListener.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectEventSourceListener.java index e33b0456835a1..8341d5e47cb31 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectEventSourceListener.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectEventSourceListener.java @@ -27,6 +27,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.sse.InboundSseEvent; @@ -138,9 +139,14 @@ public void onError(Throwable error) { // seconds. So we wait few seconds before trying again. if (error instanceof NotAuthorizedException) { logger.debug( - "Event source listener connection failure due to unauthorized exception : wait 10 seconds... haId={}", + "Event source listener connection failure due to unauthorized exception : wait 20 seconds... haId={}", haId); - scheduledExecutorService.schedule(() -> eventListener.onClosed(), 10, TimeUnit.SECONDS); + scheduledExecutorService.schedule(() -> eventListener.onClosed(), 20, TimeUnit.SECONDS); + } else if (error instanceof InternalServerErrorException) { + logger.debug( + "Event source listener connection failure due to internal server exception : wait 2 seconds... haId={}", + haId); + scheduledExecutorService.schedule(() -> eventListener.onClosed(), 2, TimeUnit.SECONDS); } else { eventListener.onClosed(); } diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectStreamingRequestFilter.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectStreamingRequestFilter.java index c93acd25bc135..66984e8917894 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectStreamingRequestFilter.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HomeConnectStreamingRequestFilter.java @@ -13,14 +13,19 @@ package org.openhab.binding.homeconnect.internal.client; import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; import javax.ws.rs.client.ClientRequestContext; import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.ClientResponseContext; +import javax.ws.rs.client.ClientResponseFilter; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Inserts Authorization header for requests on the streaming REST API. @@ -28,23 +33,49 @@ * @author Laurent Garnier - Initial contribution */ @NonNullByDefault -public class HomeConnectStreamingRequestFilter implements ClientRequestFilter { +public class HomeConnectStreamingRequestFilter implements ClientRequestFilter, ClientResponseFilter { private static final String TEXT_EVENT_STREAM = "text/event-stream"; - private final String authorizationHeader; - - public HomeConnectStreamingRequestFilter(String authorizationHeader) { - this.authorizationHeader = authorizationHeader; - } + private final Logger logger = LoggerFactory.getLogger(HomeConnectStreamingRequestFilter.class); + private final ConcurrentHashMap authorizationHeaders = new ConcurrentHashMap<>(); @Override public void filter(@Nullable ClientRequestContext requestContext) throws IOException { if (requestContext != null) { MultivaluedMap headers = requestContext.getHeaders(); - headers.putSingle(HttpHeaders.AUTHORIZATION, authorizationHeader); + String authorizationHeader = authorizationHeaders.get(requestContext.getUri().toString()); + if (authorizationHeader != null) { + headers.putSingle(HttpHeaders.AUTHORIZATION, authorizationHeader); + } else { + logger.warn("No authorization header set! uri={}", requestContext.getUri()); + } headers.putSingle(HttpHeaders.CACHE_CONTROL, "no-cache"); headers.putSingle(HttpHeaders.ACCEPT, TEXT_EVENT_STREAM); } } + + @Override + public void filter(@Nullable ClientRequestContext requestContext, @Nullable ClientResponseContext responseContext) + throws IOException { + if (logger.isDebugEnabled() && requestContext != null) { + StringBuilder sb = new StringBuilder(); + sb.append("SSE connection: "); + sb.append(requestContext.getUri()).append("\n"); + requestContext.getHeaders() + .forEach((name, value) -> sb.append("> ").append(name).append(": ").append(value).append("\n")); + + if (responseContext != null) { + responseContext.getHeaders() + .forEach((name, value) -> sb.append("< ").append(name).append(": ").append(value).append("\n")); + } + + logger.debug("{}", sb); + } + } + + public void setAuthorizationHeader(String target, String header) { + logger.debug("Set authorization header. target={}, header={}", target, header); + authorizationHeaders.put(target, header); + } } diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HttpHelper.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HttpHelper.java index c03f589611599..3fdaedb380781 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HttpHelper.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/client/HttpHelper.java @@ -60,6 +60,8 @@ public class HttpHelper { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final JsonParser JSON_PARSER = new JsonParser(); private static final Map BUCKET_MAP = new HashMap<>(); + private static final Object AUTHORIZATION_HEADER_MONITOR = new Object(); + private static final Object BUCKET_MONITOR = new Object(); private static @Nullable String lastAccessToken = null; @@ -90,36 +92,42 @@ public static String formatJsonBody(@Nullable String jsonString) { public static String getAuthorizationHeader(OAuthClientService oAuthClientService) throws AuthorizationException, CommunicationException { - try { - AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse(); - // refresh the token if it's about to expire - if (accessTokenResponse != null - && accessTokenResponse.isExpired(LocalDateTime.now(), OAUTH_EXPIRE_BUFFER)) { - LoggerFactory.getLogger(HttpHelper.class).debug("Requesting a refresh of the access token."); - accessTokenResponse = oAuthClientService.refreshToken(); - } + synchronized (AUTHORIZATION_HEADER_MONITOR) { + try { + AccessTokenResponse accessTokenResponse = oAuthClientService.getAccessTokenResponse(); + // refresh the token if it's about to expire + if (accessTokenResponse != null + && accessTokenResponse.isExpired(LocalDateTime.now(), OAUTH_EXPIRE_BUFFER)) { + LoggerFactory.getLogger(HttpHelper.class).debug("Requesting a refresh of the access token."); + accessTokenResponse = oAuthClientService.refreshToken(); + } - if (accessTokenResponse != null) { - String lastToken = lastAccessToken; - if (lastToken == null) { - LoggerFactory.getLogger(HttpHelper.class).debug("The used access token was created at {}", - accessTokenResponse.getCreatedOn().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); - } else if (!lastToken.equals(accessTokenResponse.getAccessToken())) { - LoggerFactory.getLogger(HttpHelper.class).debug("The access token changed. New one created at {}", - accessTokenResponse.getCreatedOn().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + if (accessTokenResponse != null) { + String lastToken = lastAccessToken; + if (lastToken == null) { + LoggerFactory.getLogger(HttpHelper.class).debug("The used access token was created at {}", + accessTokenResponse.getCreatedOn().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + } else if (!lastToken.equals(accessTokenResponse.getAccessToken())) { + LoggerFactory.getLogger(HttpHelper.class).debug( + "The access token changed. New one created at {}", + accessTokenResponse.getCreatedOn().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + } + lastAccessToken = accessTokenResponse.getAccessToken(); + + LoggerFactory.getLogger(HttpHelper.class).debug("Current access token: {}", + accessTokenResponse.getAccessToken()); + return BEARER + accessTokenResponse.getAccessToken(); + } else { + LoggerFactory.getLogger(HttpHelper.class).error("No access token available! Fatal error."); + throw new AuthorizationException("No access token available!"); } - lastAccessToken = accessTokenResponse.getAccessToken(); - return BEARER + accessTokenResponse.getAccessToken(); - } else { - LoggerFactory.getLogger(HttpHelper.class).error("No access token available! Fatal error."); - throw new AuthorizationException("No access token available!"); + } catch (IOException e) { + String errorMessage = e.getMessage(); + throw new CommunicationException(errorMessage != null ? errorMessage : "IOException", e); + } catch (OAuthException | OAuthResponseException e) { + String errorMessage = e.getMessage(); + throw new AuthorizationException(errorMessage != null ? errorMessage : "oAuth exception", e); } - } catch (IOException e) { - String errorMessage = e.getMessage(); - throw new CommunicationException(errorMessage != null ? errorMessage : "IOException", e); - } catch (OAuthException | OAuthResponseException e) { - String errorMessage = e.getMessage(); - throw new AuthorizationException(errorMessage != null ? errorMessage : "oAuth exception", e); } } @@ -127,20 +135,22 @@ public static JsonElement parseString(String json) { return JSON_PARSER.parse(json); } - private static synchronized Bucket getBucket(String clientId) { - Bucket bucket = null; - if (BUCKET_MAP.containsKey(clientId)) { - bucket = BUCKET_MAP.get(clientId); - } + private static Bucket getBucket(String clientId) { + synchronized (BUCKET_MONITOR) { + Bucket bucket = null; + if (BUCKET_MAP.containsKey(clientId)) { + bucket = BUCKET_MAP.get(clientId); + } - if (bucket == null) { - bucket = Bucket4j.builder() - // allows 50 tokens per minute (added 10 second buffer) - .addLimit(classic(50, intervally(50, Duration.ofSeconds(70))).withInitialTokens(40)) - // but not often then 50 tokens per second - .addLimit(classic(10, intervally(10, Duration.ofSeconds(1)))).build(); - BUCKET_MAP.put(clientId, bucket); + if (bucket == null) { + bucket = Bucket4j.builder() + // allows 50 tokens per minute (added 10 second buffer) + .addLimit(classic(50, intervally(50, Duration.ofSeconds(70))).withInitialTokens(40)) + // but not often then 50 tokens per second + .addLimit(classic(10, intervally(10, Duration.ofSeconds(1)))).build(); + BUCKET_MAP.put(clientId, bucket); + } + return bucket; } - return bucket; } } diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java index 20af6c24c9f2d..8c15a57ef7939 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/handler/AbstractHomeConnectThingHandler.java @@ -21,6 +21,7 @@ import static org.openhab.core.thing.ThingStatus.*; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -96,6 +97,7 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i private @Nullable ScheduledFuture reinitializationFuture2; private @Nullable ScheduledFuture reinitializationFuture3; private boolean ignoreEventSourceClosedEvent; + private @Nullable String programOptionsDelayedUpdate; private final ConcurrentHashMap eventHandlers; private final ConcurrentHashMap channelUpdateHandlers; @@ -103,6 +105,7 @@ public abstract class AbstractHomeConnectThingHandler extends BaseThingHandler i private final ExpiringStateMap expiringStateMap; private final AtomicBoolean accessible; private final Logger logger = LoggerFactory.getLogger(AbstractHomeConnectThingHandler.class); + private final Map> availableProgramOptionsCache; public AbstractHomeConnectThingHandler(Thing thing, HomeConnectDynamicStateDescriptionProvider dynamicStateDescriptionProvider) { @@ -112,6 +115,7 @@ public AbstractHomeConnectThingHandler(Thing thing, this.dynamicStateDescriptionProvider = dynamicStateDescriptionProvider; expiringStateMap = new ExpiringStateMap(Duration.ofSeconds(CACHE_TTL_SEC)); accessible = new AtomicBoolean(false); + availableProgramOptionsCache = new ConcurrentHashMap<>(); configureEventHandlers(eventHandlers); configureChannelUpdateHandlers(channelUpdateHandlers); @@ -221,7 +225,7 @@ && isThingAccessibleViaServerSentEvents())) && apiClient.isPresent()) { command.toFullString(), getThingLabel(), getThingHaId(), e.getMessage()); updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); resetChannelsOnOfflineEvent(); - resetProgramStateChannels(); + resetProgramStateChannels(true); } catch (CommunicationException e) { logger.debug("Could not handle command {}. API communication problem! error={}, haId={}", command.toFullString(), e.getMessage(), getThingHaId()); @@ -241,13 +245,14 @@ public void onEvent(Event event) { getThingHaId()); updateStatus(OFFLINE); resetChannelsOnOfflineEvent(); - resetProgramStateChannels(); + resetProgramStateChannels(true); } else if (isThingOnline() && CONNECTED.equals(event.getType())) { logger.debug("Received CONNECTED event. Update power state channel. haId={}", getThingHaId()); getThingChannel(CHANNEL_POWER_STATE).ifPresent(c -> updateChannel(c.getUID())); } else if (isThingOffline() && !KEEP_ALIVE.equals(event.getType())) { updateStatus(ONLINE); logger.debug("Set {} to ONLINE and update channels. haId={}", getThing().getLabel(), getThingHaId()); + updateSelectedProgramStateDescription(); updateChannels(); } @@ -528,7 +533,7 @@ protected void updateChannel(ChannelUID channelUID) { getThingLabel(), getThingHaId(), e.getMessage()); updateStatus(OFFLINE); resetChannelsOnOfflineEvent(); - resetProgramStateChannels(); + resetProgramStateChannels(true); } catch (CommunicationException e) { logger.debug("API communication problem while trying to update! thing={}, haId={}, error={}", getThingLabel(), getThingHaId(), e.getMessage()); @@ -542,8 +547,10 @@ protected void updateChannel(ChannelUID channelUID) { /** * Reset program related channels. + * + * @param offline true if the device is considered as OFFLINE */ - protected void resetProgramStateChannels() { + protected void resetProgramStateChannels(boolean offline) { logger.debug("Resetting active program channel states. thing={}, haId={}", getThingLabel(), getThingHaId()); } @@ -770,7 +777,7 @@ protected EventHandler defaultPowerStateEventHandler() { if (STATE_POWER_ON.equals(event.getValue())) { getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID())); } else { - resetProgramStateChannels(); + resetProgramStateChannels(true); getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE) .ifPresent(c -> updateState(c.getUID(), UnDefType.UNDEF)); } @@ -798,7 +805,7 @@ protected EventHandler defaultOperationStateEventHandler() { .ifPresent(c -> updateState(c.getUID(), new QuantityType<>(0, PERCENT))); getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(c -> updateChannel(c.getUID())); } else if (STATE_OPERATION_READY.equals(event.getValue())) { - resetProgramStateChannels(); + resetProgramStateChannels(false); } }; } @@ -809,7 +816,7 @@ protected EventHandler defaultActiveProgramEventHandler() { getThingChannel(CHANNEL_ACTIVE_PROGRAM_STATE).ifPresent(channel -> updateState(channel.getUID(), value == null ? UnDefType.UNDEF : new StringType(mapStringType(value)))); if (event.getValue() == null) { - resetProgramStateChannels(); + resetProgramStateChannels(false); } }; } @@ -852,15 +859,41 @@ protected EventHandler defaultAmbientLightCustomColorStateEventHandler() { }); } + protected EventHandler updateRemoteControlActiveAndProgramOptionsStateEventHandler() { + return event -> { + defaultBooleanEventHandler(CHANNEL_REMOTE_CONTROL_ACTIVE_STATE).handle(event); + + // update available program options if update was previously delayed and remote control is enabled + try { + String programKey = programOptionsDelayedUpdate; + if (programKey != null && Boolean.parseBoolean(event.getValue())) { + logger.debug("Delayed update of options for program {}", programKey); + updateProgramOptionsStateDescriptions(programKey); + programOptionsDelayedUpdate = null; + } + } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) { + logger.debug("Could not update program options. {}", e.getMessage()); + } + }; + } + protected EventHandler updateProgramOptionsAndSelectedProgramStateEventHandler() { return event -> { defaultSelectedProgramStateEventHandler().handle(event); // update available program options try { + Optional apiClient = getApiClient(); String programKey = event.getValue(); - if (programKey != null) { - updateProgramOptionsStateDescriptions(programKey); + if (apiClient.isPresent() && programKey != null) { + // Delay the update if options are not yet cached and remote control is disabled + if (availableProgramOptionsCache.get(programKey) == null + && !apiClient.get().isRemoteControlActive(getThingHaId())) { + logger.debug("Delay update of options for program {}", programKey); + programOptionsDelayedUpdate = programKey; + } else { + updateProgramOptionsStateDescriptions(programKey); + } } } catch (CommunicationException | ApplianceOfflineException | AuthorizationException e) { logger.debug("Could not update program options. {}", e.getMessage()); @@ -1040,6 +1073,25 @@ protected ChannelUpdateHandler updateProgramOptionsStateDescriptionsAndSelectedP })); } + protected ChannelUpdateHandler getAndUpdateSelectedProgramStateUpdateHandler() { + return (channelUID, cache) -> { + Optional channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE); + if (channel.isPresent()) { + defaultSelectedProgramStateUpdateHandler().handle(channel.get().getUID(), cache); + } + }; + } + + protected ChannelUpdateHandler getAndUpdateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() { + return (channelUID, cache) -> { + Optional channel = getThingChannel(CHANNEL_SELECTED_PROGRAM_STATE); + if (channel.isPresent()) { + updateProgramOptionsStateDescriptionsAndSelectedProgramStateUpdateHandler() + .handle(channel.get().getUID(), cache); + } + }; + } + protected ChannelUpdateHandler defaultActiveProgramStateUpdateHandler() { return (channelUID, cache) -> updateState(channelUID, cache.putIfAbsentAndGet(channelUID, () -> { Optional apiClient = getApiClient(); @@ -1050,7 +1102,7 @@ protected ChannelUpdateHandler defaultActiveProgramStateUpdateHandler() { processProgramOptions(program.getOptions()); return new StringType(mapStringType(program.getKey())); } else { - resetProgramStateChannels(); + resetProgramStateChannels(false); return UnDefType.UNDEF; } } @@ -1172,6 +1224,14 @@ protected void handleLightCommands(final ChannelUID channelUID, final Command co } } + protected void handlePowerCommand(final ChannelUID channelUID, final Command command, + final HomeConnectApiClient apiClient, String stateNotOn) + throws CommunicationException, AuthorizationException, ApplianceOfflineException { + if (command instanceof OnOffType && CHANNEL_POWER_STATE.equals(channelUID.getId())) { + apiClient.setPowerState(getThingHaId(), OnOffType.ON.equals(command) ? STATE_POWER_ON : stateNotOn); + } + } + private int getCurrentBrightness(final ChannelUID channelUID, final HomeConnectApiClient apiClient) throws CommunicationException, AuthorizationException, ApplianceOfflineException { String id = channelUID.getId(); @@ -1263,6 +1323,7 @@ protected void processProgramOptions(List + veto String @@ -447,6 +449,7 @@ + veto String @@ -461,6 +464,7 @@ + veto String @@ -475,6 +479,7 @@ + veto Number:Temperature @@ -508,6 +513,7 @@ Specifies the desired dryness setting. + veto String diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CommonRpcParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CommonRpcParser.java index 7926374288529..bbf53ed51ec5e 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CommonRpcParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/CommonRpcParser.java @@ -177,12 +177,18 @@ protected HmDatapoint assembleDatapoint(String name, String unit, String type, S unit = unit.trim().replace("\ufffd", "°"); } dp.setUnit(unit == null || unit.isEmpty() ? null : unit); - if (dp.getUnit() == null && dp.getName() != null && dp.getName().startsWith("RSSI_")) { - dp.setUnit("dBm"); - } - // Bypass: For at least one device the CCU does not send a unit together with the value - if (dp.getUnit() == null && dp.getName().startsWith(HomematicConstants.DATAPOINT_NAME_OPERATING_VOLTAGE)) { - dp.setUnit("V"); + + // Bypass: For several devices the CCU does not send a unit together with the value in the data point definition + if (dp.getUnit() == null && dp.getName() != null) { + if (dp.getName().startsWith("RSSI_")) { + dp.setUnit("dBm"); + } else if (dp.getName().startsWith(HomematicConstants.DATAPOINT_NAME_OPERATING_VOLTAGE)) { + dp.setUnit("V"); + } else if (HomematicConstants.DATAPOINT_NAME_HUMIDITY.equals(dp.getName())) { + dp.setUnit("%"); + } else if (HomematicConstants.DATAPOINT_NAME_LEVEL.equals(dp.getName())) { + dp.setUnit("100%"); + } } HmValueType valueType = HmValueType.parse(type); diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/GetDeviceDescriptionParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/GetDeviceDescriptionParser.java index 68b5c9076e639..29d92f507fd74 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/GetDeviceDescriptionParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/GetDeviceDescriptionParser.java @@ -48,7 +48,7 @@ public String getType() { * Returns the parsed firmware version. */ public String getFirmware() { - return firmware; + return firmware == null ? "" : firmware; } /** diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListBidcosInterfacesParser.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListBidcosInterfacesParser.java index 53446f024cfea..cc4a8eb00a289 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListBidcosInterfacesParser.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/communicator/parser/ListBidcosInterfacesParser.java @@ -64,7 +64,7 @@ public String getGatewayAddress() { * Returns the firmware version. */ public String getFirmware() { - return firmware; + return firmware == null ? "" : firmware; } /** diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/HomematicTypeGeneratorImpl.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/HomematicTypeGeneratorImpl.java index a3ea8000e88e7..147c65c347ead 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/HomematicTypeGeneratorImpl.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/HomematicTypeGeneratorImpl.java @@ -339,7 +339,6 @@ private void generateConfigDescription(HmDevice device, URI configDescriptionURI builder.withLabel(MetadataUtils.getLabel(dp)); builder.withDefault(Objects.toString(dp.getDefaultValue(), "")); builder.withDescription(MetadataUtils.getDatapointDescription(dp)); - if (dp.isEnumType()) { builder.withLimitToOptions(dp.isEnumType()); List options = MetadataUtils.generateOptions(dp, @@ -353,8 +352,14 @@ public ParameterOption createOption(String value, String description) { } if (dp.isNumberType()) { + Number defaultValue = (Number) dp.getDefaultValue(); + Number maxValue = dp.getMaxValue(); + // some datapoints can have a default value that is greater than the maximum value + if (defaultValue.doubleValue() > maxValue.doubleValue()) { + maxValue = defaultValue; + } builder.withMinimum(MetadataUtils.createBigDecimal(dp.getMinValue())); - builder.withMaximum(MetadataUtils.createBigDecimal(dp.getMaxValue())); + builder.withMaximum(MetadataUtils.createBigDecimal(maxValue)); builder.withStepSize(MetadataUtils .createBigDecimal(dp.isFloatType() ? Float.valueOf(0.1f) : Long.valueOf(1L))); builder.withUnitLabel(MetadataUtils.getUnit(dp)); diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java index 490ce8293b165..664c9508f1c92 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java @@ -310,6 +310,7 @@ public static String getItemType(HmDatapoint dp) { return ITEM_TYPE_NUMBER + ":Temperature"; case "V": return ITEM_TYPE_NUMBER + ":ElectricPotential"; + case "100%": case "%": return ITEM_TYPE_NUMBER + ":Dimensionless"; case "mHz": @@ -341,7 +342,6 @@ public static String getItemType(HmDatapoint dp) { case "day": case "month": case "year": - case "100%": default: return ITEM_TYPE_NUMBER; } diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/generator/CcuMetadataExtractor.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/generator/CcuMetadataExtractor.java index 0dc6f526521e9..74f0e77942390 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/generator/CcuMetadataExtractor.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/generator/CcuMetadataExtractor.java @@ -82,7 +82,7 @@ private void generate() throws IOException { file.delete(); } file.createNewFile(); - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "ISO-8859-1")); + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")); // write device descriptions bw.write("# -------------- generated descriptions " + new Date() + " --------------\n"); @@ -216,7 +216,7 @@ public UrlLoader(String url) throws IOException { public UrlLoader(String url, String startLine, String endLine) throws IOException { System.out.println("Loading file " + url); Boolean includeLine = null; - BufferedReader br = new BufferedReader(new InputStreamReader(new URL(url).openStream(), "ISO-8859-1")); + BufferedReader br = new BufferedReader(new InputStreamReader(new URL(url).openStream(), "UTF-8")); String line; while ((line = br.readLine()) != null) { diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions.properties b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions.properties index 75630cfa14edf..f9c53bf1f26e8 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions.properties +++ b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions.properties @@ -1,4 +1,16 @@ GATEWAY-EXTRAS=Gateway extras, variables and scripts +ASIR=Homematic IP Indoor Siren +FAL230-C10=Homematic IP Floor Heating Actuator +FAL230-C6=Homematic IP Floor Heating Actuator +FAL24-C10=Homematic IP Floor Heating Actuator +FAL24-C6=Homematic IP Floor Heating Actuator +STHD=Homematic IP Temperature and Humidity Sensor with Display, indoor +SWD=Homematic IP Window/Door Contact optical +WRC2=Homematic IP Wall-mount Remote Control 2 buttons +WRC6=Homematic IP Wall-mount Remote Control 6 buttons +WS888=Wireless Weather Data Center +WTH=Homematic IP Wall Thermostat + HM-RC-4-2=Remote Control 4 buttons HM-LC-Sw1-Pl-2=Wireless Switch Actuator 1-channel, socket adapter HM-Sec-SD-2=Wireless Smoke Detector @@ -19,12 +31,7 @@ BC-SC-Rd-WM-2=MAX! Windows sensor # Homematic IP HmIP-HAP=Homematic IP Access Point -HmIP-RCV-50=Homematic IP virtual remote control -HmIP-SWDO=Homematic IP Door-/window contact, optical -HmIP-WTH=Homematic IP Wall thermostat with humidity sensor HmIP-STH=Homematic IP Wall temperature and humidity sensor -HmIP-STHD=Homematic IP Wall temperature and humidity sensor with display -HMIP-WRC2=Homematic IP Wall mount remote control 2 button HMIP-PS=Homematic IP Pluggable switch HMIP-PSM=Homematic IP Pluggable switch and meter HMIP-eTRV=Homematic IP Radiator thermostat @@ -39,78 +46,15 @@ HmIP-RC8=Homematic IP Remote Control - 8 buttons HmIP-DRA=Homematic IP DIN-Rail Adapter for Multi IO Box HmIP-WRC6=Homematic IP Wall-mount Remote Control - 6 buttons HmIP-FAL24-TR=Homematic IP Transformer for Floor Heating Actuators - 24 V -HmIP-FAL24-C6=Homematic IP Floor Heating Actuator - 6 channels, 24 V -HmIP-FAL24-C10=Homematic IP Floor Heating Actuator - 6 channels, 24 V HmIP-SRH=Homematic IP Window Handle Sensor -HmIP-WTH-B=Homematic IP Wall Thermostat - basic -HmIP-WTH-2=Homematic IP Wall thermostat with humidity sensor -HmIP-FAL230-C6=Homematic IP Floor Heating Actuator - 6 channels, 230 V -HmIP-FAL230-C10=Homematic IP Floor Heating Actuator - 6 channels, 230 V -HmIP-FALMOT-C12=Homematic IP Floor Heating Actuator - 12 channels, motorised HmIP-BDT=Homematic IP Dimming Actuator for brand switches HmIP-BSM=Homematic IP Switch Actuator and Meter for brand switches HmIP-FSM=Homematic IP Switch Actuator and Meter - flush-mount HmIP-SWSD=Homematic IP Smoke Alarm -HmIP-ASIR=Homematic IP Alarm Siren -HmIP-ASIR-O=Homematic IP Alarm Siren - outdoor HmIP-PDT-UK=Pluggable Dimmer - trailing edge (UK) HmIP-eTRV-UK=Radiator Thermostat (UK) -HmIP-SWDO-I=Window / Door Contact - invisible installation -HmIP-SWDO-PL=Homematic IP Window / Door Contact - optical, plus -HmIP-SWO-B=Homematic IP Weather Sensor - basic -HmIP-SWO-PL=Homematic IP Weather Sensor - plus -HmIP-SWO-PR=Homematic IP Weather Sensor - pro -HmIP-WGC=Homematic IP Garage Door Controller -HmIP-WHS2=Homematic IP Switch Actuator for heating systems - 2 channels -HmIP-WRCD=Homematic IP Wall-mount Remote Control with status display -HmIP-WRCR=Homematic IP Rotary Button HmIP-BWTH=Wall Thermostat with switching output HmIP-BWTH24=Wall Thermostat with switching output -HmIP-PCBS-BAT=Circuit board for battery operation -HmIP-PCBS2=Homematic IP Switch Circuit Board - 2 channels -HmIP-PMFS=Homematic IP Mains Failure Surveillance -HmIP-SCI=Homematic IP Contact Interface -HmIP-SLO=Homematic IP Light Sensor - outdoor -HmIP-SMI55=Homematic IP Motion Detector for 55mm frames - indoor -HmIP-SPDR=Homematic IP Passage Sensor with Direction Recognition -HmIP-SRD=Homematic IP Rain Sensor -HmIP-STV=Homematic IP Tilt and Vibration Sensor -HmIP-FCI1=Homematic IP Contact Interface flush-mount - 1 channel -HmIP-FCI6=Homematic IP Contact Interface flush-mount - 6 channels -HmIP-FROLL=Homematic IP Wireless Blind Actuator -HmIP-BROLL=Homematic IP Wireless Blind Actuator for brand switches -HmIP-FSI16=Homematic IP Switch Actuator with Push-button Input (16 A) - flush-mount -HmIP-MIO16-PCB=Homematic IP Multi IO Module Board - 4x4 -HmIP-BRC2=Homematic IP Wall-mount Remote Control for brand switches - 2 channels -HmIP-MOD-TM=Homematic IP Garage Door Module -HmIP-MOD-HO=Homematic IP Module for Hoermann drives -HmIP-MOD-RC8=Homematic IP Module Board Transmitter - 8 channels -HmIP-MP3P=Homematic IP Combination Signalling Device MP3 -HmIP-SWD=Homematic IP Water Sensor -HmIP-SWDM=Homematic IP Window / Door Contact with magnet -HmIP-BSL=Homematic IP Switch Actuator for brand switches – with signal lamp -HmIP-DBB=Homematic IP Doorbell Button -HmIP-DRBLI4=Homematic IP Blind and Shutter Actuator for DIN rail mount - 4 channels -HmIP-DRDI3=Homematic IP Dimming Actuator for DIN rail mount - 3 channels -HmIP-DRSI4=Homematic IP Switch Actuator for DIN rail mount - 4 channels -HmIP-DSD-PCB=Homematic IP Doorbell Sensor -HmIPW-DRAP=Homematic IP Wired Access Point -HmIPW-DRBL4=Homematic IP Wired Blind and Shutter Actuator - 4 channels -HmIPW-DRD3=Homematic IP Wired Dimming Actuator - 3 channels -HmIPW-DRI16=Homematic IP Wired Input Module - 16 channels -HmIPW-DRI32=Homematic IP Wired Input Module - 32 channels -HmIPW-DRS4=Homematic IP Wired Switch Actuator - 4 channels -HmIPW-DRS8=Homematic IP Wired Switch Actuator - 8 channels -HmIPW-FAL230-C6=Homematic IP Wired Floor Heating Actuator - 6 channels, 230 V -HmIPW-FAL230-C10=Homematic IP Wired Floor Heating Actuator - 10 channels, 230 V -HmIPW-FAL24-C6=Homematic IP Wired Floor Heating Actuator - 6 channels, 24 V -HmIPW-FAL24-C10=Homematic IP Wired Floor Heating Actuator - 10 channels, 24 V -HmIPW-FIO6=Homematic IP Wired IO Module flush-mount - 6 channels -HmIPW-SMI55=Homematic IP Wired Motion Detector for 55mm frames - indoor -HmIPW-SPI=Homematic IP Wired Presence sensor - indoor -HmIPW-STH=Homematic IP Wired Temperature and Humidity Sensor - indoor -HmIPW-STHD=Homematic IP Wired Temperature and Humidity Sensor with display - indoor -HmIPW-WTH=Homematic IP Wired Wall Thermostat with Humidity Sensor # virtual datapoints DELETE_DEVICE_MODE=Deletemode @@ -140,7 +84,6 @@ CHANNEL_NAME=Channel # More examples: # ROTARY_HANDLE_SENSOR|STATE|CLOSED=Window status: locked ########################################################################################################## - RSSI_PEER=Signalstrength Peer INSTALL_TEST=Installationtest DUTY_CYCLE=Time limit @@ -184,7 +127,25 @@ POWERMETER_IGL|METER_TYPE|UNKNOWN=Unknown POWERMETER_IGL|METER_TYPE|UNKOWN=Unknown POWERMETER_IGL|BOOT=Boot ROTARY_HANDLE_SENSOR|ERROR|=Unknown - +ROTARY_HANDLE_SENSOR|EVENT_DELAYTIME=Message delay +WEATHER|AIR_PRESSURE=Barometric pressure +WEATHER|BRIGHTNESS=Brightness +WEATHER|HUMIDITY=Relative humidity +WEATHER|RAINING=Rain +WEATHER|RAINING|FALSE=currently not raining +WEATHER|RAINING|TRUE=currently raining +WEATHER|RAIN_COUNTER=Rainfall +WEATHER|STORM_LOWER_THRESHOLD=Wind alert off threshold +WEATHER|STORM_UPPER_THRESHOLD=Wind alert on threshold +WEATHER|SUNSHINEDURATION=Sunshine duration +WEATHER|SUNSHINE_THRESHOLD=Sunshine threshold +WEATHER|TEMPERATURE=Temperature +WEATHER|WIND_DIRECTION=Wind direction +WEATHER|WIND_DIRECTION_RANGE=Wind direction fluctuation range +WEATHER|WIND_SPEED=Wind velocity +WEATHER|WIND_SPEED_RESULT_SOURCE=Type of wind velocity value +WEATHER|WIND_SPEED_RESULT_SOURCE|AVERAGE_VALUE=Average +WEATHER|WIND_SPEED_RESULT_SOURCE|MAX_VALUE=Maximum value EVENT_DELAY_UNIT|100MS=100ms EVENT_DELAY_UNIT|S=seconds EVENT_DELAY_UNIT|M=minutes @@ -193,6 +154,8 @@ EVENT_RANDOMTIME_UNIT|100MS=100ms EVENT_RANDOMTIME_UNIT|S=seconds EVENT_RANDOMTIME_UNIT|M=minutes EVENT_RANDOMTIME_UNIT|H=hours +DIMMER|LEVEL=Dimming value +DIMMER|LEVEL_REAL=Dimming value real channel TX_MINDELAY_UNIT|100MS=100ms TX_MINDELAY_UNIT|S=seconds TX_MINDELAY_UNIT|M=minutes @@ -214,7 +177,9 @@ MAINTENANCE|BATTERY_TYPE=Type of the battery MAINTENANCE|FIRMWARE=Version of the firmware SWITCH_TRANSMITTER|STATE=Switching status SWITCH_VIRTUAL_RECEIVER|STATE=Switching status +ENERGIE_METER_TRANSMITTER|CURRENT=Current ENERGIE_METER_TRANSMITTER|TX_THRESHOLD_ENERGY=Configuration Value for the minimal ENERGY_COUNTER change for sending event in 0.1Wh +ENERGIE_METER_TRANSMITTER|VOLTAGE=Voltage COND_SWITCH_TRANSMITTER|COND_TX_THRESHOLD_LO=Configuration Value for the value of the low threshold COND_SWITCH_TRANSMITTER|COND_TX_THRESHOLD_HI=Configuration Value for the value of the high threshold SHUTTER_CONTACT|STATE_CONTACT=State as contact diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions_de.properties b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions_de.properties index 9a6b4893223c3..53d668b9f3c42 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions_de.properties +++ b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/extra-descriptions_de.properties @@ -1,17 +1,29 @@ GATEWAY-EXTRAS=Gateway extras, Variablen und Scripte +ASIR=Homematic IP Innensirene +FAL230-C10=Homematic IP Fussbodenheizungsaktor +FAL230-C6=Homematic IP Fussbodenheizungsaktor +FAL24-C10=Homematic IP Fussbodenheizungsaktor +FAL24-C6=Homematic IP Fussbodenheizungsaktor +STHD=Homematic IP Temperatur- und Luftfeuchtigkeitssensor mit Display, innen +SWD=Homematic IP Fenster- und Türkontakt optisch +WRC2=Homematic IP Wandtaster 2-fach +WRC6=Homematic IP Wandtaster 6-fach +WS888=Funk-Wetterstation +WTH=Homematic IP Wandthermostat + HM-RC-4-2=Funk-Handsender 4 Tasten HM-LC-Sw1-Pl-2=Funk-Schaltaktor 1-fach, Zwischenstecker HM-Sec-SD-2=Funk-Rauchmelder HM-WDS30-OT2-SM-2=Funk-Temperaturdifferenzsensor -HM-Sen-MDIR-O-2=Funk-Bewegungsmelder auen -HM-ES-TX-WM2=Funk-Sender fr Energiezhler-Sensor Version 2 +HM-Sen-MDIR-O-2=Funk-Bewegungsmelder außen +HM-ES-TX-WM2=Funk-Sender für Energiezähler-Sensor Version 2 # MAX! -BC-RT-TRX-CyN=MAX! Heizkrperthermostat Basic -BC-RT-TRX-CyG=MAX! Elektronischer Heizkrperthermostat +BC-RT-TRX-CyN=MAX! Heizkörperthermostat Basic +BC-RT-TRX-CyG=MAX! Elektronischer Heizkörperthermostat BC-RT-TRX-CyG-2=MAX! Wandthermostat WT+ -BC-RT-TRX-CyG-3=MAX! Heizkrperthermostat -BC-RT-TRX-CyG-4=MAX! Heizkrperthermostat+ +BC-RT-TRX-CyG-3=MAX! Heizkörperthermostat +BC-RT-TRX-CyG-4=MAX! Heizkörperthermostat+ BC-TS-Sw-Pl=MAX! Zwischenstecker BC-PB-2-WM-2=MAX! Eco Taster BC-TC-C-WM-4=MAX! Wandthermostat+ @@ -19,135 +31,67 @@ BC-SC-Rd-WM-2=MAX! Fensterkontakt # Homematic IP HmIP-HAP=Homematic IP Access Point -HmIP-RCV-50=Homematic IP virtuelle Fernbedienung -HmIP-SWDO=Homematic IP Fenster- und Trkontakt, optisch -HmIP-WTH=Homematic IP Wandthermostat mit Luftfeuchtigkeitssensor HmIP-STH=Homematic IP Temperatur- und Luftfeuchtigkeitssensor - innen -HmIP-STHD=Homematic IP Temperatur- und Luftfeuchtigkeitssensor mit Display - innen -HMIP-WRC2=Homematic IP Wandtaster, 2-fach HMIP-PS=Homematic IP Schaltsteckdose HMIP-PSM=Homematic IP Schalt-Mess-Steckdose -HMIP-eTRV=Homematic IP Heizkrperthermostat -HMIP-eTRV-2=Homematic IP Heizkrperthermostat -HMIP-eTRV-B=Homematic IP Heizkrperthermostat Basis -HMIP-eTRV-C=Homematic IP Heizkrperthermostat kompakt -HmIP-eTRV-C-2=Homematic IP Heizkrperthermostat kompakt -HmIP-SMI=Homematic IP Bewegungsmelder mit Dmmerungssensor -HmIP-SMO=Homematic IP Bewegungsmelder mit Dmmerungssensor auen -HmIP-KRC4=Homematic IP Schlsselbundfernbedienung - 4 Tasten +HMIP-eTRV=Homematic IP Heizkörperthermostat +HMIP-eTRV-2=Homematic IP Heizkörperthermostat +HMIP-eTRV-B=Homematic IP Heizkörperthermostat Basis +HMIP-eTRV-C=Homematic IP Heizkörperthermostat kompakt +HmIP-eTRV-C-2=Homematic IP Heizkörperthermostat kompakt +HmIP-SMI=Homematic IP Bewegungsmelder mit Dämmerungssensor +HmIP-SMO=Homematic IP Bewegungsmelder mit Dämmerungssensor außen +HmIP-KRC4=Homematic IP Schlüsselbundfernbedienung - 4 Tasten HmIP-RC8=Homematic IP Fernbedienung - 8 Tasten -HmIP-DRA=Homematic IP Hutschienenadapter fr Multi IO Box +HmIP-DRA=Homematic IP Hutschienenadapter für Multi IO Box HmIP-WRC6=Homematic IP Wandtaster - 6-fach -HmIP-FAL24-TR=Homematic IP Trafo fr Fubodenheizungsaktoren - 24 V -HmIP-FAL24-C6=Homematic IP Fubodenheizungsaktor - 6-fach, 24 V -HmIP-FAL24-C10=Homematic IP Fubodenheizungsaktor - 6-fach, 24 V +HmIP-FAL24-TR=Homematic IP Trafo für Fußbodenheizungsaktoren - 24 V HmIP-SRH=Homematic IP Fenstergriffsensor -HmIP-WTH-B=Homematic IP Wandthermostat - basic -HmIP-WTH-2=Homematic IP Wandthermostat mit Luftfeuchtigkeitssensor -HmIP-FAL230-C6=Homematic IP Fubodenheizungsaktor - 6-fach, 230 V -HmIP-FAL230-C10=Homematic IP Fubodenheizungsaktor - 6-fach, 230 V -HmIP-FALMOT-C12=Homematic IP Fubodenheizungsaktor - 12-fach, motorisch -HmIP-BDT=Homematic IP Dimmaktor fr Markenschalter -HmIP-BSM=Homematic IP Schalt-Mess-Aktor fr Markenschalter +HmIP-BDT=Homematic IP Dimmaktor für Markenschalter +HmIP-BSM=Homematic IP Schalt-Mess-Aktor für Markenschalter HmIP-FSM=Homematic IP Schalt-Mess-Aktor - Unterputz HmIP-SWSD=Homematic IP Rauchwarnmelder -HmIP-ASIR=Homematic IP Alarmsirene -HmIP-ASIR-O=Homematic IP Alarmsirene - auen HmIP-PDT-UK=Dimmer-Steckdose - Phasenabschnitt (UK-Version) -HmIP-eTRV-UK=Heizkrperthermostat (UK-Version) -HmIP-SWDO-I=Fenster- und Trkontakt - verdeckten Einbau -HmIP-SWDO-PL=Homematic IP Fenster- und Trkontakt - optisch, plus -HmIP-SWO-B=Homematic IP Wettersensor - basic -HmIP-SWO-PL=Homematic IP Wettersensor - plus -HmIP-SWO-PR=Homematic IP Wettersensor - pro -HmIP-WGC=Homematic IP Garagentortaster -HmIP-WHS2=Homematic IP Schaltaktor fr Heiz-Systeme - 2 Kanal -HmIP-WRCD=Homematic IP Wandtaster mit Statusdisplay -HmIP-WRCR=Homematic IP Drehtaster +HmIP-eTRV-UK=Heizkörperthermostat (UK-Version) HmIP-BWTH=Wandthermostat mit Schaltausgang HmIP-BWTH24=Wandthermostat mit Schaltausgang -HmIP-PCBS-BAT=Schaltplatine fr Batteriebetrieb -HmIP-PCBS2=Homematic IP Schaltplatine - 2-fach -HmIP-PMFS=Homematic IP Netzausfallberwachung -HmIP-SCI=Homematic IP Kontakt-Schnittstelle -HmIP-SLO=Homematic IP Lichtsensor - auen -HmIP-SMI55=Homematic IP Bewegungsmelder fr 55er Rahmen - innen -HmIP-SPDR=Homematic IP Durchgangssensor mit Richtungserkennung -HmIP-SRD=Homematic IP Regensensor -HmIP-STV=Homematic IP Neigungs- und Erschtterungssensor -HmIP-FCI1=Homematic IP Kontakt-Schnittstelle Unterputz - 1-fach -HmIP-FCI6=Homematic IP Kontakt-Schnittstelle Unterputz - 6-fach -HmIP-FROLL=Homematic IP Rolladenaktor -HmIP-BROLL=Homematic IP Rolladenaktor fr Markenschalter -HmIP-FSI16=Homematic IP Schaltaktor mit Tastereingang (16 A) - Unterputz -HmIP-MIO16-PCB=Homematic IP Multi IO Modulpatine - 4x4 -HmIP-BRC2=Homematic IP Wandtaster fr Markenschalter - 2-fach -HmIP-MOD-TM=Homematic IP Tormatic Modul -HmIP-MOD-HO=Homematic IP Modul fr Hrmann-Antriebe -HmIP-MOD-RC8=Homematic IP Modulplatine Sender - 8-fach -HmIP-MP3P=Homematic IP MP3 Kombisignalgeber -HmIP-SWD=Homematic IP Wassersensor -HmIP-SWDM=Homematic IP Fenster- und Trkontakt mit Magnet -HmIP-BSL=Homematic IP Schaltaktor fr Markenschalter mit Signalleuchte -HmIP-DBB=Homematic IP Trklingeltaster -HmIP-DRBLI4=Homematic IP Jalousie- u. Rollladenaktor fr Hutschienenmontage - 4-fach -HmIP-DRDI3=Homematic IP Dimmaktor fr Hutschienenmontage - 3-fach -HmIP-DRSI4=Homematic IP Schaltaktor fr Hutschienenmontage - 4-fach -HmIP-DSD-PCB=Homematic IP Klingelsignalsensor -HmIPW-DRAP=Homematic IP Wired Access Point -HmIPW-DRBL4=Homematic IP Wired Jalousie- u. Rollladenaktor - 4-fach -HmIPW-DRD3=Homematic IP Wired Dimmaktor - 3-fach -HmIPW-DRI16=Homematic IP Wired Eingangsmodul - 16-fach -HmIPW-DRI32=Homematic IP Wired Eingangsmodul - 32-fach -HmIPW-DRS4=Homematic IP Wired Schaltaktor - 4-fach -HmIPW-DRS8=Homematic IP Wired Schaltaktor - 8-fach -HmIPW-FAL230-C6=Homematic IP Wired Fubodenheizungsaktor - 6-fach, 230 V -HmIPW-FAL230-C10=Homematic IP Wired Fubodenheizungsaktor - 10-fach, 230 V -HmIPW-FAL24-C6=Homematic IP Wired Fubodenheizungsaktor - 6-fach, 24 V -HmIPW-FAL24-C10=Homematic IP Wired Fubodenheizungsaktor - 10-fach, 24 V -HmIPW-FIO6=Homematic IP Wired IO Modul Unterputz - 6-fach -HmIPW-SMI55=Homematic IP Wired Bewegungsmelder fr 55er Rahmen - innen -HmIPW-SPI=Homematic IP Wired Prsenzmelder - innen -HmIPW-STH=Homematic IP Wired Temperatur- und Luftfeuchtigkeitssensor - innen -HmIPW-STHD=Homematic IP Wired Temperatur- und Luftfeuchtigkeitssensor mit Display - innen -HmIPW-WTH=Homematic IP Wired Wandthermostat mit Luftfeuchtigkeitssensor # Virtuelle Datenpunkte -DELETE_DEVICE_MODE=Lschmodus -DELETE_DEVICE=Gert lschen -DISPLAY_OPTIONS=Zeige optionen am Display an +DELETE_DEVICE_MODE=Löschmodus +DELETE_DEVICE=Gerät löschen +DISPLAY_OPTIONS=Zeige Optionen am Display an ON_TIME_AUTOMATIC=Automatische Einschaltdauer INSTALL_MODE_DURATION=Dauer des Anlernmodus -INSTALL_MODE=Gert anlernen +INSTALL_MODE=Gerät anlernen RELOAD_ALL_FROM_GATEWAY=Alle Daten vom Gateway neu laden -MAINTENANCE|DELETE_DEVICE_MODE|LOCKED=Lschen gesperrt -MAINTENANCE|DELETE_DEVICE_MODE|RESET=Lschen und Gert auf Werkseinstellunen zurcksetzen -MAINTENANCE|DELETE_DEVICE_MODE|FORCE=Lschen, auch wenn das Geert nicht erreichbar ist -MAINTENANCE|DELETE_DEVICE_MODE|DEFER=Bei nchster Gelegenheit lschen +MAINTENANCE|DELETE_DEVICE_MODE|LOCKED=Löschen gesperrt +MAINTENANCE|DELETE_DEVICE_MODE|RESET=Löschen und Gerät auf Werkseinstellungen zurücksetzen +MAINTENANCE|DELETE_DEVICE_MODE|FORCE=Löschen, auch wenn das Gerät nicht erreichbar ist +MAINTENANCE|DELETE_DEVICE_MODE|DEFER=Bei nächster Gelegenheit löschen CHANNEL_NAME=Kanal ####################################### Datenpunkt Beschreibungen ########################################## -# Du kannst hier Datenpunkt Beschreibungen hinzufgen im Format: +# Du kannst hier Datenpunkt Beschreibungen hinzufügen im Format: # KANAL_TYP|DATENPUNKT_NAME|OPTIONS_WERT # -# Beispiel: Eine allgemeine Beschreibung fr den LEVEL Datenpunkt: +# Beispiel: Eine allgemeine Beschreibung für den LEVEL Datenpunkt: # LEVEL=Level Beschreibung -# Jedoch wird LEVEL in div. Gerten verwendet und hat eine unterschiedliche Bedeutung, daher kannst Du den Kanaltyp voranstellen: +# Jedoch wird LEVEL in div. Geräten verwendet und hat eine unterschiedliche Bedeutung, daher kannst Du den Kanaltyp voranstellen: # DIMMER|LEVEL=Dimmwert -# BLIND|LEVEL=Behanghhe +# BLIND|LEVEL=Behanghöhe # Weiteres Beispiel: # ROTARY_HANDLE_SENSOR|STATE|CLOSED=Fensterzustand: verriegelt ########################################################################################################## -RSSI_PEER=Signalstrke Peer +RSSI_PEER=Signalstärke Peer INSTALL_TEST=Installationstest DUTY_CYCLE=Sendezeitbegrenzung SWITCH|STATE=Schaltzustand WORKING=In Betrieb BLIND|DIRECTION=Laufrichtung -AES_ACTIVE=AES-Verschlsselung -AES_KEY=AES-Schlssel +AES_ACTIVE=AES-VerSchlüsselung +AES_KEY=AES-Schlüssel DUTYCYCLE=Sendezeitbegrenzung SMOKE_DETECTOR|STATE=Raucherkennung SMOKE_DETECTOR_TEAM|STATE=Team Raucherkennung @@ -155,22 +99,22 @@ DIMMER|DIRECTION=Dimmrichtung PRESS_CONT=Tastendruck durchgehend PRESS_LONG_RELEASE=Tastendruck lang ende ROTARY_HANDLE_SENSOR|STATE=Fensterposition -SENSOR=Ereignisauslsung +SENSOR=Ereignisauslösung DISPLAY|UNIT=Einheit DISPLAY|BEEP=Ton -POWERMETER_IGL|METER_CONSTANT_GAS=Gas-Zhlerkonstante -POWERMETER_IGL|METER_CONSTANT_LED=LED-Zhlerkonstante -POWERMETER_IGL|METER_CONSTANT_IR=IR-Zhlerkonstante +POWERMETER_IGL|METER_CONSTANT_GAS=Gas-Zählerkonstante +POWERMETER_IGL|METER_CONSTANT_LED=LED-Zählerkonstante +POWERMETER_IGL|METER_CONSTANT_IR=IR-Zählerkonstante POWERMETER_IGL|METER_TYPE=Sensor-Typ POWERMETER_IGL|METER_SENSIBILITY_IR=IR-Empfindlichkeitsschwelle -COND_TX_THRESHOLD_HI_CURRENT=Ein Wert wird gesendet, wenn sich seit der letzten Sendung der Strom gendert hat -COND_TX_THRESHOLD_LO_CURRENT=Ein Wert wird gesendet, wenn sich seit der letzten Sendung der Strom gendert hat -COND_TX_THRESHOLD_LO_FREQUENCY=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Spannung gendert hat -COND_TX_THRESHOLD_HI_FREQUENCY=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Spannung gendert hat -COND_TX_THRESHOLD_HI_VOLTAGE=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Spannung gendert hat -COND_TX_THRESHOLD_LO_VOLTAGE=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Spannung gendert hat -COND_TX_THRESHOLD_HI_POWER=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Leistung gendert hat -COND_TX_THRESHOLD_LO_POWER=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Leistung gendert hat +COND_TX_THRESHOLD_HI_CURRENT=Ein Wert wird gesendet, wenn sich seit der letzten Sendung der Strom geändert hat +COND_TX_THRESHOLD_LO_CURRENT=Ein Wert wird gesendet, wenn sich seit der letzten Sendung der Strom geändert hat +COND_TX_THRESHOLD_LO_FREQUENCY=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Spannung geändert hat +COND_TX_THRESHOLD_HI_FREQUENCY=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Spannung geändert hat +COND_TX_THRESHOLD_HI_VOLTAGE=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Spannung geändert hat +COND_TX_THRESHOLD_LO_VOLTAGE=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Spannung geändert hat +COND_TX_THRESHOLD_HI_POWER=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Leistung geändert hat +COND_TX_THRESHOLD_LO_POWER=Ein Wert wird gesendet, wenn sich seit der letzten Sendung die Leistung geändert hat DIRECTION|NONE=Keine DIRECTION|UP=Hoch DIRECTION|DOWN=Runter @@ -183,7 +127,25 @@ POWERMETER_IGL|METER_TYPE|UNKNOWN=Unbekannt POWERMETER_IGL|METER_TYPE|UNKOWN=Unbekannt POWERMETER_IGL|BOOT=Boot ROTARY_HANDLE_SENSOR|ERROR|=Unbekannt - +ROTARY_HANDLE_SENSOR|EVENT_DELAYTIME=Meldeverzögerung +WEATHER|AIR_PRESSURE=Luftdruck +WEATHER|BRIGHTNESS=Helligkeit +WEATHER|HUMIDITY=Rel. Luftfeuchte +WEATHER|RAINING=Regen +WEATHER|RAINING|FALSE=aktuell kein Regen +WEATHER|RAINING|TRUE=aktuell Regen +WEATHER|RAIN_COUNTER=Regenmenge +WEATHER|STORM_LOWER_THRESHOLD=Windalarm-Ausschaltschwelle +WEATHER|STORM_UPPER_THRESHOLD=Windalarm-Einschaltschwelle +WEATHER|SUNSHINEDURATION=Sonnenscheindauer +WEATHER|SUNSHINE_THRESHOLD=Sonnenscheinschwelle +WEATHER|TEMPERATURE=Temperatur +WEATHER|WIND_DIRECTION=Windrichtung +WEATHER|WIND_DIRECTION_RANGE=Windrichtung Schwankungsbreite +WEATHER|WIND_SPEED=Windgeschwindigkeit +WEATHER|WIND_SPEED_RESULT_SOURCE=Art des Windgeschwindigkeitswertes +WEATHER|WIND_SPEED_RESULT_SOURCE|AVERAGE_VALUE=Mittelwert +WEATHER|WIND_SPEED_RESULT_SOURCE|MAX_VALUE=Maximalwert EVENT_DELAY_UNIT|100MS=100ms EVENT_DELAY_UNIT|S=Sekunden EVENT_DELAY_UNIT|M=Minuten @@ -192,30 +154,34 @@ EVENT_RANDOMTIME_UNIT|100MS=100ms EVENT_RANDOMTIME_UNIT|S=Sekunden EVENT_RANDOMTIME_UNIT|M=Minuten EVENT_RANDOMTIME_UNIT|H=Stunden +DIMMER|LEVEL=Dimmwert +DIMMER|LEVEL_REAL=Dimmwert Realkanal TX_MINDELAY_UNIT|100MS=100ms TX_MINDELAY_UNIT|S=Sekunden TX_MINDELAY_UNIT|M=Minuten TX_MINDELAY_UNIT|H=Stunden MAINTENANCE|ENABLE_ROUTING=Konfigurationswert zum aktivieren / deaktivieren des Routings -MAINTENANCE|CYCLIC_INFO_MSG_OVERDUE_THRESHOLD=Anzahl der erlaubten berfllig zyklischen Info-Meldungen, bis ein UNREACH Ereignis ausgelst wird -MAINTENANCE|DUTYCYCLE_LIMIT=Konfigurationswert fr die Dutycycle Grenze -MAINTENANCE|LOCAL_RESET_DISABLED=Konfigurationswert um den lokale Reset des Gerts zu deaktivieren -MAINTENANCE|CYCLIC_INFO_MSG_DIS_UNCHANGED=Anzahl der zu berspringenden zyklischen Info-Meldungen durch ein Gert, damit der Status unverndert bleibt -MAINTENANCE|ARR_TIMEOUT=Konfigurationswert fr das Antwort-Timeout der Anwendung in Sekunden +MAINTENANCE|CYCLIC_INFO_MSG_OVERDUE_THRESHOLD=Anzahl der erlaubten überfälligen zyklischen Info-Meldungen, bis ein UNREACH Ereignis ausgelöst wird +MAINTENANCE|DUTYCYCLE_LIMIT=Konfigurationswert für die Dutycycle Grenze +MAINTENANCE|LOCAL_RESET_DISABLED=Konfigurationswert um den lokalen Reset des Geräts zu deaktivieren +MAINTENANCE|CYCLIC_INFO_MSG_DIS_UNCHANGED=Anzahl der zu überspringenden zyklischen Info-Meldungen durch ein Gerät, damit der Status unverändert bleibt +MAINTENANCE|ARR_TIMEOUT=Konfigurationswert für das Antwort-Timeout der Anwendung in Sekunden MAINTENANCE|UPDATE_PENDING=Zeigt eine ausstehende Aktualisierung an -MAINTENANCE|UNREACH=Gertekommunikationsstatus -MAINTENANCE|STICKY_UNREACH=Gertekommunikationsstatus -MAINTENANCE|CONFIG_PENDING=Zeigt eine ausstehende Konfigurations Aktualisierung an -MAINTENANCE|DEVICE_IN_BOOTLOADER=Gert im bootloader -MAINTENANCE|RSSI_DEVICE=Empfangsstrke -MAINTENANCE|RSSI=Vereinheitlichte Empfangsstrke +MAINTENANCE|UNREACH=Gerätekommunikationsstatus +MAINTENANCE|STICKY_UNREACH=Gerätekommunikationsstatus +MAINTENANCE|CONFIG_PENDING=Zeigt eine ausstehende Konfigurationsaktualisierung an +MAINTENANCE|DEVICE_IN_BOOTLOADER=Gerät im Bootloader +MAINTENANCE|RSSI_DEVICE=Empfangsstärke +MAINTENANCE|RSSI=Vereinheitlichte Empfangsstärke MAINTENANCE|BATTERY_TYPE=Typ der Batterie MAINTENANCE|FIRMWARE=Version der Firmware SWITCH_TRANSMITTER|STATE=Schaltstatus SWITCH_VIRTUAL_RECEIVER|STATE=Schaltstatus -ENERGIE_METER_TRANSMITTER|TX_THRESHOLD_ENERGY=Konfigurationswert fr die minimale nderung des Energie-Zhlers, um ein Ereignis in 0.1Wh zu senden +ENERGIE_METER_TRANSMITTER|CURRENT=Strom +ENERGIE_METER_TRANSMITTER|TX_THRESHOLD_ENERGY=Konfigurationswert für die minimale Änderung des Energie-Zählers, um ein Ereignis in 0.1Wh zu senden +ENERGIE_METER_TRANSMITTER|VOLTAGE=Spannung COND_SWITCH_TRANSMITTER|COND_TX_THRESHOLD_LO=Konfigurationswert der unteren Schwelle -COND_SWITCH_TRANSMITTER|COND_TX_THRESHOLD_HI=Konfigurationswert der hohen Schwelle +COND_SWITCH_TRANSMITTER|COND_TX_THRESHOLD_HI=Konfigurationswert der oberen Schwelle SHUTTER_CONTACT|STATE_CONTACT=Schaltzustand als Kontakt KEY|DISPLAY_LINE_1=Display Zeile 1 @@ -224,22 +190,22 @@ KEY|DISPLAY_LINE_3=Display Zeile 3 KEY|DISPLAY_LINE_4=Display Zeile 4 KEY|DISPLAY_LINE_5=Display Zeile 5 -KEY|DISPLAY_COLOR_1=Farbe fr Zeile 1 -KEY|DISPLAY_COLOR_2=Farbe fr Zeile 2 -KEY|DISPLAY_COLOR_3=Farbe fr Zeile 3 -KEY|DISPLAY_COLOR_4=Farbe fr Zeile 4 -KEY|DISPLAY_COLOR_5=Farbe fr Zeile 5 +KEY|DISPLAY_COLOR_1=Farbe für Zeile 1 +KEY|DISPLAY_COLOR_2=Farbe für Zeile 2 +KEY|DISPLAY_COLOR_3=Farbe für Zeile 3 +KEY|DISPLAY_COLOR_4=Farbe für Zeile 4 +KEY|DISPLAY_COLOR_5=Farbe für Zeile 5 -KEY|DISPLAY_ICON_1=Icon fr Zeile 1 -KEY|DISPLAY_ICON_2=Icon fr Zeile 2 -KEY|DISPLAY_ICON_3=Icon fr Zeile 3 -KEY|DISPLAY_ICON_4=Icon fr Zeile 4 -KEY|DISPLAY_ICON_5=Icon fr Zeile 5 +KEY|DISPLAY_ICON_1=Icon für Zeile 1 +KEY|DISPLAY_ICON_2=Icon für Zeile 2 +KEY|DISPLAY_ICON_3=Icon für Zeile 3 +KEY|DISPLAY_ICON_4=Icon für Zeile 4 +KEY|DISPLAY_ICON_5=Icon für Zeile 5 KEY|DISPLAY_BEEPCOUNT=Anzahl Beeps (0 = unendlich) KEY|DISPLAY_BEEPINTERVAL=Abstand zwischen zwei Beeps in Schritten von 10s (10 - 160s) -KEY|DISPLAY_SUBMIT=Displaynachricht bertragen +KEY|DISPLAY_SUBMIT=Displaynachricht übertragen NONE=Keine OFF=Aus @@ -250,15 +216,15 @@ OK=OK INFO=Info NEW_MESSAGE=Neue Nachricht SERVICE=Service -SIGNAL_GREEN=Grnes Signal +SIGNAL_GREEN=Grünes Signal SIGNAL_YELLOW=Gelbes Signal SIGNAL_RED=Rotes Signal -WHITE=Wei +WHITE=Weiß RED=Rot ORANGE=Orange YELLOW=Gelb -GREEN=Grn +GREEN=Grün BLUE=Blau DISPLAY_BEEPER|LONG_LONG=Lang lang diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/generated-descriptions.properties b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/generated-descriptions.properties index c355860ac8dc5..ce0ba01d9b8b0 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/generated-descriptions.properties +++ b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/generated-descriptions.properties @@ -1,4 +1,4 @@ -# -------------- generated descriptions Fri Jul 21 16:59:59 CEST 2017 -------------- +# -------------- generated descriptions Sun Apr 25 19:44:22 CEST 2021 -------------- # DON'T CHANGE THIS FILE! 263_130=Wireless Switch Actuator 1-channel, flush-mount @@ -11,25 +11,20 @@ 263_145=Wireless Push-button interface 4-channel, flush-mount 263_146=Wireless Blind Actuator 1-channel, flush-mount 263_147=Wireless Shutter Actuator 1-channel, surface-mount -263_149_/_263_150=engl Schco WCS-TipTronic-Platine +263_149_/_263_150=engl Schüco WCS-TipTronic-Platine 263_155=Wireless Display Push-button 2-channel, surface-mount -263_157=Wireless Temperature Sensor, indoor +263_157=Wireless Temperature Sensor - indoor 263_158=Wireless Temperature/Humidity Sensor, outdoor 263_160=Wireless Sensor for Carbon Dioxide -263_162=Wireless Motion Detector, indoor +263_162=Wireless Motion Detector - indoor 263_167=Wireless Smoke Detector 263_167_Gruppe=Wireless Smoke Detector (Group) ALPHA-IP-RBG=Room Control Unit Display ALPHA-IP-RBGa=Room Control Unit Analogue -ASIR=Homematic IP Indoor Siren BDT=Homematic IP Dimming Actuator for brand switch systems, flush-mount BRC-H=Remote Control DORMA, 4-channel BSM=Homematic IP Switch Actuator with power measurement DEVICE=Unknown device -FAL230-C10=Homematic IP Floor Heating Actuator -FAL230-C6=Homematic IP Floor Heating Actuator -FAL24-C10=Homematic IP Floor Heating Actuator -FAL24-C6=Homematic IP Floor Heating Actuator FDT=Homematic IP Dimming Actuator, flush-mount FSM=Homematic IP Switch Actuator with power measurement, flush-mount FSM16=Homematic IP Switch Actuator with power measurement, flush-mount @@ -38,7 +33,7 @@ HM-CC-SCD=Wireless Sensor for Carbon Dioxide HM-CC-TC=Wireless Wall Thermostat HM-CC-VD=Wireless Valve Drive HM-CC-VG-1=Group heating control -HM-CCU-1=HomeMatic Central Control Unit +HM-CCU-1=Homematic Central Control Unit HM-DW-WM=Wireless Dimming Actuator 2-channel PWM LED HM-Dis-EP-WM55=Display Status Monitor with E-Paper-Display HM-Dis-TD-T=Wireless Status Monitor @@ -139,18 +134,19 @@ HM-SCI-3-FM=Wireless Shutter Contact Interface 3-channel, flush-mount HM-Sec-Key=KeyMatic HM-Sec-Key-O=KeyMatic HM-Sec-Key-S=KeyMatic -HM-Sec-MDIR=Wireless Motion Detector, indoor +HM-Sec-MDIR=Wireless Motion Detector - indoor HM-Sec-RHS=Wireless Window Rotary Handle Sensor HM-Sec-SC=Wireless Door/Window Contact HM-Sec-SC-2=Wireless Door/Window Contact HM-Sec-SCo=Wireless Door/Window Contact optical HM-Sec-SD=Wireless Smoke Detector +HM-Sec-SD-Team=Wireless Smoke Detector (Group) HM-Sec-SFA-SM=Wireless Siren/Flash Actuator HM-Sec-Sir-WM=Wireless Indoor Siren HM-Sec-TiS=Wireless Tilt Sensor HM-Sec-WDS=Wireless Water Detection Sensor +HM-Sec-WDS-2=Wireless Water Detection Sensor HM-Sec-Win=WinMatic -HM-Sec_SD-Team=Wireless Smoke Detector (Group) HM-Sen-DB-PCB=Wireless Doorbell Sensor HM-Sen-EP=Wireless sensor for electrical pulses HM-Sen-LI-O=Wireless Light Intensity Sensor, outdoor @@ -167,10 +163,10 @@ HM-WDS10-TH-O=Wireless Temperature/Humidity Sensor, outdoor HM-WDS100-C6-O=Wireless Weather Data Sensor OC 3 HM-WDS30-OT2-SM=Wireless Temperature Difference Sensor HM-WDS30-T-O=Wireless Temperature Sensor, outdoor -HM-WDS40-TH-I=Wireless Temperature Sensor, indoor +HM-WDS40-TH-I=Wireless Temperature Sensor - indoor HM-WS550-US=Wireless Weather Data Center USA HM-WS550ST-IO=Wireless Temperature Sensor, outdoor -HM-WS550STH-I=Wireless Temperature Sensor, indoor +HM-WS550STH-I=Wireless Temperature Sensor - indoor HM-WS550STH-O=Wireless Temperature/Humidity Sensor, outdoor HMW-IO-12-FM=Wired RS485 I/O Module 12-channel, flush-mount HMW-IO-12-Sw14-DR=Wired RS485 I/O Module with 12 inputs, 14 outputs, DIN rail mount @@ -186,15 +182,96 @@ HMW-Sen-SC-12-FM=Wired RS485 Shutter Contact 12-channel, flush-mount HMW-Sys-PS7-DR=Wired RS485 Power Supply 7 VA, DIN rail mount HMW-WSE-SM=Wired RS485 Light Sensor, surface mount HMW-WSTH-SM=Wired RS485 Temperature/Humidity Sensor -HmIP-BBL=Homematic IP Jalousieaktor for brand switch systems, flush-mount +HmIP-ASIR=Homematic IP Alarm Siren +HmIP-ASIR-O=Homematic IP Alarm Siren - outdoor +HmIP-BBL=Homematic IP Blinds Actuator for brand switches +HmIP-BRC2=Homematic IP Remote Control for brand switches - 2 channels HmIP-BROLL=Homematic IP Blind Actuator for brand switch systems, flush-mount -HmIP-FBL=Homematic IP Jalousieaktor, flush-mount +HmIP-BSL=Homematic IP Switch Actuator with Signal Lamp - for brand switches +HmIP-CCU3=Homematic IP Central Control Unit CCU3 +HmIP-DBB=Homematic IP Doorbell Button +HmIP-DLD=Homematic IP Door Lock Drive +HmIP-DRBLI4=Homematic IP Blind and Shutter Actuator for DIN rail mount - 4 channels +HmIP-DRDI3=Homematic IP Dimming Actuator for DIN rail mount - 3 channels +HmIP-DRS4=Homematic IP Switch Actuator for DIN rail mount - 4-fach +HmIP-DRSI1=Homematic IP Switch Actuator for DIN rail mount - 1 channel +HmIP-DRSI4=Homematic IP Switch Actuator for DIN rail mount - 4 channels +HmIP-DSD-PCB=Homematic IP Doorbell Sensor +HmIP-FAL230-C10=Homematic IP Floor Heating Actuator - 10 channels 230 V +HmIP-FAL230-C6=Homematic IP Floor Heating Actuator - 6 channels 230 V +HmIP-FAL24-C10=Homematic IP Floor Heating Actuator - 10 channels 24 V +HmIP-FAL24-C6=Homematic IP Floor Heating Actuator - 6 channels 24 V +HmIP-FALMOT-C12=Homematic IP Floor Heating Actuator - 12 channels, motorised +HmIP-FBL=Homematic IP Blinds Actuator - flush-mount +HmIP-FCI1=Homematic IP Contact Interface flush-mount - 1 channel +HmIP-FCI6=Homematic IP Contact Interface flush-mount - 6 channels HmIP-FROLL=Homematic IP Blind Actuator, flush-mount +HmIP-FSI16=Homematic IP Switch Actuator with Push-button Input (16 A) - flush-mount +HmIP-HAP=LAN ROUTER +HmIP-KRCK=Homematic IP Key Ring Remote Control - access control +HmIP-MIO16-PCB=Homematic IP Multi IO Module Board - 4x4 +HmIP-MOD-HO=Homematic IP Module for Hoermann drives HmIP-MOD-OC8=Homematic IP Switch Actuator with OC-Output -HmIP-PCBS=Homematic IP Wireless Switch Actuator 1-channel, PCB +HmIP-MOD-RC8=Homematic IP Module Board Transmitter - 8 channels +HmIP-MOD-TM=Homematic IP Tormatic Module +HmIP-MP3P=Homematic IP Combination Signalling Device MP3 +HmIP-PCBS=Homematic IP Switch Circuit Board +HmIP-PCBS-BAT=Homematic IP Switch Circuit for battery operation +HmIP-PCBS2=Homematic IP Switch Circuit Board - 2 channels +HmIP-PMFS=Homematic IP Mains Failure Surveillance +HmIP-RCV-50=Virtual Remote Control HmIP-SAM=Homematic IP Acceleration Sensor -HmIP-SPI=Homematic IP Presence sensor, indoor -HmIP-STHO=Homematic IP Temperature and Humidity Sensor outdoor +HmIP-SCI=Homematic IP Contact Interface +HmIP-SCTH230=Homematic IP CO2 Sensor, 230 V +HmIP-SFD=Homematic IP Particulate Matter Sensor +HmIP-SLO=Homematic IP Light Sensor - outdoor +HmIP-SMI55=Homematic IP Motion Detector for 55mm frames - indoor +HmIP-SPDR=Homematic IP Passage Sensor with Direction Recognition +HmIP-SPI=Homematic IP Presence sensor - indoor +HmIP-SRD=Homematic IP Rain Sensor +HmIP-STE1-PCB=Homematic IP Temperature Sensor with external probe - 1 channels +HmIP-STE2-PCB=Homematic IP Temperature Sensor with external probes - 2 channels +HmIP-STHD=Homematic IP Temperature and Humidity Sensor with Display - indoor +HmIP-STHO=Homematic IP Temperature and Humidity Sensor - outdoor +HmIP-STV=Homematic IP Tilt and Vibration Sensor +HmIP-SWD=Homematic IP Water Sensor +HmIP-SWDM=Homematic IP Window / Door Contact with magnet +HmIP-SWDO=Homematic IP Window / Door Contact - optical +HmIP-SWDO-I=Homematic IP Window / Door Contact - invisible installation +HmIP-SWDO-PL=Homematic IP Window / Door Contact - optical, plus +HmIP-SWO-B=Homematic IP Weather Sensor - basic +HmIP-SWO-PL=Homematic IP Weather Sensor - plus +HmIP-SWO-PR=Homematic IP Weather Sensor - pro +HmIP-USBSM=Homematic IP Switch Actuator and Meter for USB +HmIP-WGC=Homematic IP Garage Door Controller +HmIP-WHS2=Homematic IP Switch Actuator for heating systems - 2 channels +HmIP-WRC2=Homematic IP Wall-mount Remote Control 2 buttons +HmIP-WRCC2=Homematic IP Wall-mount Remote Control - flat +HmIP-WRCD=Homematic IP Wall-mount Remote Control with status display +HmIP-WRCR=Homematic IP Rotary Button +HmIP-WTH=Homematic IP Wall Thermostat +HmIP-WTH-2=Homematic IP Wall Thermostat with Humidity Sensor +HmIP-WTH-B=Homematic IP Wall Thermostat - basic +HmIPW-BRC2=Homematic IP Wired Remote Control for brand switches - 2 channels +HmIPW-DRAP=Homematic IP Wired Access Point +HmIPW-DRBL4=Homematic IP Wired Blind and Shutter Actuator - 4 channels +HmIPW-DRD3=Homematic IP Wired Dimming Actuator - 3 channels +HmIPW-DRI16=Homematic IP Wired Input Module - 16 channels +HmIPW-DRI32=Homematic IP Wired Input Module - 32 channels +HmIPW-DRS4=Homematic IP Wired Switch Actuator - 4 channels +HmIPW-DRS8=Homematic IP Wired Switch Actuator - 8 channels +HmIPW-FAL230-C10=Homematic IP Wired Floor Heating Actuator - 10 channels, 230 V +HmIPW-FAL230-C6=Homematic IP Wired Floor Heating Actuator - 6 channels, 230 V +HmIPW-FAL24-C10=Homematic IP Wired Floor Heating Actuator - 10 channels, 24 V +HmIPW-FAL24-C6=Homematic IP Wired Floor Heating Actuator - 6 channels, 24 V +HmIPW-FIO6=Homematic IP Wired IO Module flush-mount - 6 channels +HmIPW-SMI55=Homematic IP Wired Motion Detector for 55mm frames - indoor +HmIPW-SPI=Homematic IP Wired Presence sensor - indoor +HmIPW-STH=Homematic IP Wired Temperature and Humidity Sensor - indoor +HmIPW-STHD=Homematic IP Wired Temperature and Humidity Sensor with display - indoor +HmIPW-WRC2=Homematic IP Wired Wall-mount Remote Control - 2 buttons +HmIPW-WRC6=Homematic IP Wired Wall-mount Remote Control - 6 buttons +HmIPW-WTH=Homematic IP Wired Wall Thermostat with Humidity Sensor KRC4=Homematic IP Key Ring Remote Control - 4 buttons KRCA=Homematic IP Key Ring Remote Control - 4 buttons Alarm KS550=Wireless Weather Data Sensor 550 @@ -208,20 +285,24 @@ PSM-IT=Homematic IP Pluggable Switch and Meter IT PSM-PE=Homematic IP Pluggable Switch and Meter Pin Earth PSM-UK=Homematic IP Pluggable Switch and Meter UK RC8=Homematic IP Remote Contro, 8-channel -SMI=Homematic IP Motion Detector, indoor +RPI-RF-MOD=CO-PROCESSOR +SMI=Homematic IP Motion Detector - indoor SMO=Homematic IP Motion Detector, outdoor SRH=Homematic IP Rotary Handle Sensor -STH=Homematic IP Temperature and Humidity Sensor, indoor -STHD=Homematic IP Temperature and Humidity Sensor with Display, indoor -SWD=Homematic IP Window/Door Contact optical +STH=Homematic IP Temperature and Humidity Sensor - indoor SWSD=Homematic IP Smoke Detector TRV=Homematic IP Radiator Thermostat +TRV-B=Homematic IP Radiator Thermostat - basic +TRV-B-UK=Homematic IP Radiator Thermostat - basic UK +TRV-C=Homematic IP Radiator Thermostat - compact +TRV-E=Homematic IP Radiator Thermostat - Evo TRV-UK=Homematic IP Radiator Thermostat UK +The END= +VIR-HUE-GTW=Philips-Hue Gateway VIR-OL-GTW=OSRAM-Lightify Gateway -WRC2=Homematic IP Wall-mount Remote Control 2 buttons WRC6=Homematic IP Wall-mount Remote Control 6 buttons WS888=Wireless Weather Data Center -WTH=Homematic IP Wall Thermostat +WT=Homematic IP Wall Thermostat ZEL_STG_RM_DWT_10=Wireless Display Push-button 2-channel, surface-mount ZEL_STG_RM_FDK=Wireless Window Rotary Handle Sensor ZEL_STG_RM_FEP_230V=Wireless Blind Actuator 1-channel, flush-mount @@ -240,7 +321,7 @@ atent=Remote Control DORMA ACCELERATION_TRANSCEIVER=Vibration/Acceleration Sensor ACCELERATION_TRANSCEIVER|MOTION|FALSE=no motion/horizontal -ACCELERATION_TRANSCEIVER|MOTION|TRUE=motion detected/vertical +ACCELERATION_TRANSCEIVER|MOTION|TRUE=motion detected/tilted ACCELERATION_TRANSCEIVER|MSG_FOR_POS_A=Message in position vertical ACCELERATION_TRANSCEIVER|MSG_FOR_POS_A|CLOSED=closed ACCELERATION_TRANSCEIVER|MSG_FOR_POS_A|NO_MSG=no message @@ -249,6 +330,8 @@ ACCELERATION_TRANSCEIVER|MSG_FOR_POS_B=Message in position horizontal ACCELERATION_TRANSCEIVER|MSG_FOR_POS_B|CLOSED=closed ACCELERATION_TRANSCEIVER|MSG_FOR_POS_B|NO_MSG=no message ACCELERATION_TRANSCEIVER|MSG_FOR_POS_B|OPEN=open +ACOUSTIC_ALARM_ACTIVE|FALSE=Acoustic signal deactivated +ACOUSTIC_ALARM_ACTIVE|TRUE=Acoustic signal activated ACOUSTIC_ALARM_SELECTION|DELAYED_EXTERNALLY_ARMED=Absence mode delayed ACOUSTIC_ALARM_SELECTION|DELAYED_INTERNALLY_ARMED=Presence mode delayed ACOUSTIC_ALARM_SELECTION|DISABLE_ACOUSTIC_SIGNAL=No acoustic signal @@ -267,8 +350,10 @@ ACOUSTIC_ALARM_SELECTION|FREQUENCY_RISING=Frequency increasing ACOUSTIC_ALARM_SELECTION|FREQUENCY_RISING_AND_FALLING=Frequency increasing/dropping ACOUSTIC_ALARM_SELECTION|INTERNALLY_ARMED=Presence mode ACOUSTIC_ALARM_SELECTION|LOW_BATTERY=Battery empty +ACOUSTIC_SIGNAL_VIRTUAL_RECEIVER=MP3-Player ACTUAL_HUMIDITY=Relative humidity ACTUAL_TEMPERATURE=Actual temperature +AIR_PRESSURE=Barometric pressure AKKU|LEVEL=Charging status AKKU|STATUS|CHARGE=Loading AKKU|STATUS|DISCHARGE=Battery supplied @@ -284,6 +369,8 @@ ALARMTIME_MAX=Max. alarm duration ALARM_SWITCH_VIRTUAL_RECEIVER=Alarm siren ALL_LEDS=Set all channels ANALOG_INPUT=Analog +ANALOG_INPUT_TRANSMITTER|FILTER_SIZE=Number of measurements used for the average value of the input voltage. +ANALOG_INPUT_TRANSMITTER|VOLTAGE=Input voltage ANALOG_OUTPUT=Analog ANALOG_OUTPUT_TRANSCEIVER=Analogue output ANALOG_OUTPUT_TRANSCEIVER|LEVEL=Output level @@ -305,9 +392,12 @@ ARMSTATE|ALLSENS_ARMED=All sensors armed, (absence mode) ARMSTATE|DISARMED=Protection deactivated ARMSTATE|EXTSENS_ARMED=Outdoor sensors armed, (presence mode) ARR_TIMEOUT=Time-out for bidirectional communication +ATC_ADAPTION_INTERVAL=Interval for temperature compensation of the sensors +ATC_MODE=Temperature compensation of the sensors ATC_OFF=Off ATC_ON=On AUTO_MODE=Auto mode +AVERAGE_ILLUMINATION=Average level of brightness BACKLIGHT_AT_CHARGE=Back light while device is in charging station BACKLIGHT_AT_KEYSTROKE=Back light at button press BACKLIGHT_AT_MOTION=Back light at movement/vibration @@ -316,10 +406,23 @@ BATTERY_POWERED=Battery operation BATTERY_STATE=Battery status BAT_DEFECT_LIMIT=Defect battery threshold BLIND=Blind actuator +BLIND_TRANSMITTER|ACTIVITY_STATE|DOWN=Blind moves down +BLIND_TRANSMITTER|ACTIVITY_STATE|STABLE=Blind not moving +BLIND_TRANSMITTER|ACTIVITY_STATE|UNKNOWN=Blind activity unknown +BLIND_TRANSMITTER|ACTIVITY_STATE|UP=Blind moves up BLIND_TRANSMITTER|LEVEL=Blind level BLIND_TRANSMITTER|LEVEL_2=Slats position +BLIND_TRANSMITTER|PROCESS|NOT_STABLE=Blind moves +BLIND_TRANSMITTER|PROCESS|STABLE=Blind not moving +BLIND_VIRTUAL_RECEIVER=Blind actuator +BLIND_VIRTUAL_RECEIVER|ACTIVITY_STATE|DOWN=Blind moves down +BLIND_VIRTUAL_RECEIVER|ACTIVITY_STATE|STABLE=Blind not moving +BLIND_VIRTUAL_RECEIVER|ACTIVITY_STATE|UNKNOWN=Blind activity unknown +BLIND_VIRTUAL_RECEIVER|ACTIVITY_STATE|UP=Blind moves up BLIND_VIRTUAL_RECEIVER|LEVEL=Blind level BLIND_VIRTUAL_RECEIVER|LEVEL_2=Slats position +BLIND_VIRTUAL_RECEIVER|PROCESS|NOT_STABLE=Blind moves +BLIND_VIRTUAL_RECEIVER|PROCESS|STABLE=Blind not moving BLIND_VIRTUAL_RECEIVER|STOP=Stop BLIND|CHANGE_OVER_DELAY=Blind direction switch-over time BLIND|LEVEL=Blind level @@ -327,12 +430,16 @@ BLIND|REFERENCE_RUNNING_TIME_BOTTOM_TOP=Running time bottom to top BLIND|REFERENCE_RUNNING_TIME_TOP_BOTTOM=Running time top to bottom BLIND|REFERENCE_RUN_COUNTER=Number of runs until automatic calibration drive BLIND|STOP=Stop +BLOCKING_PERIOD_UNIT=Time interval unit +BLOCKING_PERIOD_VALUE=Time interval value BOOST_MODE=Boost function BOOST_MODE|FALSE=Boost mode OFF BOOST_MODE|TRUE=Boost mode ON -BOOST_STATE=Boost duration +BOOST_STATE=Boost state +BOOST_TIME=Boost duration BRIGHTNESS=Brightness BRIGHTNESS_FILTER=Brightness filter +BRIGHTNESS_TRANSMITTER|FILTER_SIZE=Number of last brightness values used for calculation of brightness BURST_RX=Wake on radio BUTTON_LOCK=Keypad lock BUTTON_RESPONSE_WITHOUT_BACKLIGHT=Immediate reaction to keypress without previous display backlight @@ -382,11 +489,19 @@ CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|HUMIDITY_LIMIT_DISABLE=Humidity threshol CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|HUMIDITY_LIMIT_VALUE=Humidity threshold CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|MINIMAL_FLOOR_TEMPERATURE=Minimum floor temperature CLIMATECONTROL_FLOOR_PUMP_TRANSCEIVER=Floor heating/Pump control +CLIMATECONTROL_FLOOR_PUMP_TRANSCEIVER|EMERGENCY_OPERATION|FALSE=Connection with room control unit OK +CLIMATECONTROL_FLOOR_PUMP_TRANSCEIVER|EMERGENCY_OPERATION|TRUE=Connection failure with room control unit CLIMATECONTROL_FLOOR_TRANSCEIVER=Floor heating +CLIMATECONTROL_FLOOR_TRANSCEIVER|EMERGENCY_OPERATION|FALSE=Connection with room control unit OK +CLIMATECONTROL_FLOOR_TRANSCEIVER|EMERGENCY_OPERATION|TRUE=Connection failure with room control unit CLIMATECONTROL_FLOOR_TRANSMITTER=Room thermostat +CLIMATECONTROL_FLOOR_TRANSMITTER|EMERGENCY_OPERATION|FALSE=Connection with room control unit OK +CLIMATECONTROL_FLOOR_TRANSMITTER|EMERGENCY_OPERATION|TRUE=Connection failure with room control unit CLIMATECONTROL_HEAT_DEMAND_BOILER_TRANSMITTER=Channel heating demand CLIMATECONTROL_HEAT_DEMAND_PUMP_TRANSMITTER=Channel heating demand CLIMATECONTROL_HEAT_DEMAND_TRANSMITER=Channel heating demand +CLIMATECONTROL_INPUT_RECEIVER=Channel heating/cooling +CLIMATECONTROL_INPUT_TRANSMITTER=Input channel CLIMATECONTROL_RECEIVER=Radiator Thermostat (Receiver wall mounted thermostat) CLIMATECONTROL_REGULATOR=Radiator thermostat CLIMATECONTROL_REGULATOR|ADJUSTING_COMMAND=Adjustment command @@ -440,14 +555,24 @@ CLIMATECONTROL_VENT_DRIVE|ERROR|VALVE_DRIVE_LOOSE=The valve drive is not install CLIMATECONTROL_VENT_DRIVE|VALVE_ERROR_POSITION=Valve drive fault position CLIMATECONTROL_VENT_DRIVE|VALVE_OFFSET_VALUE=Valve drive offset position CLIMATECONTROL_VENT_DRIVE|VALVE_STATE=Valve drive position +COLOR|LEVEL=Brightness +COMBINED_PARAMETER=Channel action COMFORT_MODE=Comfort temperature COMPATIBILITY_MODE=Compatibility mode CONDITION_CURRENT=Current sensor CONDITION_FREQUENCY=Frequency sensor CONDITION_POWER=Power sensor CONDITION_VOLTAGE=Voltage sensor -COND_TX_CYCLIC_ABOVE=Send decision value cyclically if upper limit is exceeded -COND_TX_CYCLIC_BELOW=Send decision value cyclically if lower limit falls below threshold +COND_SWITCH_TRANSMITTER=Transmitter decision value +COND_SWITCH_TRANSMITTER_BRIGHTNESS=Brightness sensor +COND_SWITCH_TRANSMITTER_HUMIDITY=Humidity sensor +COND_SWITCH_TRANSMITTER_RAIN_DROP=Rain sensor +COND_SWITCH_TRANSMITTER_RAIN_QUANTITY=Sensor rainfall volume +COND_SWITCH_TRANSMITTER_TEMPERATURE=Temperature sensor +COND_SWITCH_TRANSMITTER_WIND_DIRECTION=Wind direction sensor +COND_SWITCH_TRANSMITTER_WIND_SPEED=Wind velocity sensor +COND_TX_CYCLIC_ABOVE=Send decision value cyclically +COND_TX_CYCLIC_BELOW=Send decision value cyclically COND_TX_DECISION_ABOVE=Sent decision value if upper limit is exceeded COND_TX_DECISION_BELOW=Sent decision value if lower limit falls below threshold COND_TX_FALLING=Send decision value if lower limit falls below threshold + @@ -462,13 +587,17 @@ CONTROL_MODE|AUTO-MODE=Auto mode CONTROL_MODE|BOOST-MODE=Boost function CONTROL_MODE|MANU-MODE=Manu mode CONTROL_MODE|PARTY-MODE=Holiday mode +CURRENT=Current CURRENTDETECTION_BEHAVIOR=Response CURRENTDETECTION_BEHAVIOR|"CURRENTDETECTION_ACTIVE"=Two-way circuit CURRENTDETECTION_BEHAVIOR|"CURRENTDETECTION_INACTIVE_VALUE_OUTPUT_1"=Output 1 active CURRENTDETECTION_BEHAVIOR|"CURRENTDETECTION_INACTIVE_VALUE_OUTPUT_2"=Output 2 active +CURRENT_ILLUMINATION=Unfiltered, current level of brightness +CURRENT_PASSAGE_DIRECTION|FALSE=Passage detected: No +CURRENT_PASSAGE_DIRECTION|TRUE=Passage detected: Yes CYCLIC_INFO_MSG=Cyclically status message CYCLIC_INFO_MSG_DIS=Number of messages that are left out -CYCLIC_INFO_MSG_DIS_UNCHANGED=Number of unchangeable status messages that are left out +CYCLIC_INFO_MSG_DIS_UNCHANGED=Number of unchanged status messages that are left out CYCLIC_INFO_MSG_OVERDUE_THRESHOLD=Number of missed status messages until 'unreach' is flagged CYCLIC_INFO_MSG_PAUSE=Interval for cyclically status messages DATE_TIME_UNKNOWN|FALSE=Correct time known @@ -501,7 +630,19 @@ DIGITAL_OUTPUT=Digital DIGITAL_OUTPUT|STATE|FALSE=Switching status: off DIGITAL_OUTPUT|STATE|TRUE=Switching status: on DIMMER=Dimming actuator +DIMMER_TRANSMITTER|ACTIVITY_STATE|DOWN=Ramp down +DIMMER_TRANSMITTER|ACTIVITY_STATE|STABLE=Level stable +DIMMER_TRANSMITTER|ACTIVITY_STATE|UNKNOWN=Dimmer activity unknown +DIMMER_TRANSMITTER|ACTIVITY_STATE|UP=Ramp up +DIMMER_TRANSMITTER|PROCESS|NOT_STABLE=Ramp active +DIMMER_TRANSMITTER|PROCESS|STABLE=Level stable DIMMER_VIRTUAL_RECEIVER=Dimming actuator +DIMMER_VIRTUAL_RECEIVER|ACTIVITY_STATE|DOWN=Ramp down +DIMMER_VIRTUAL_RECEIVER|ACTIVITY_STATE|STABLE=Level stable +DIMMER_VIRTUAL_RECEIVER|ACTIVITY_STATE|UNKNOWN=Dimmer activity unknown +DIMMER_VIRTUAL_RECEIVER|ACTIVITY_STATE|UP=Ramp up +DIMMER_VIRTUAL_RECEIVER|PROCESS|NOT_STABLE=Ramp active +DIMMER_VIRTUAL_RECEIVER|PROCESS|STABLE=Level stable DIMMER|CHARACTERISTIC=Output characteristic DIMMER|ERROR_OVERHEAT=Overheat DIMMER|ERROR_OVERLOAD=Overload @@ -521,12 +662,15 @@ DIMMER|RAMP_TIME=Dimming time DIMMER|REDUCE_LEVEL=Reducing level over temperature DIMMER|REDUCE_TEMP_LEVEL=Reducing threshold over temperature DIMMER|RELAY_OFFDELAY_TIME=Relay time for switch off delay +DISABLE_ACOUSTIC_CHANNELSTATE=Deactivate buzzer +DISABLE_ACOUSTIC_SENDSTATE=Deactivate acoustic confirmation of button press DISPLAY_BACKLIGHT_MODE=Display back light mode DISPLAY_BACKLIGHT_MODE|AUTO=automatic DISPLAY_BACKLIGHT_MODE|OFF=off DISPLAY_BACKLIGHT_MODE|ON=on DISPLAY_BACKLIGHT_TIME=Display back light time DISPLAY_BRIGHTNESS=Display brightness +DISPLAY_CONTRAST=Display contrast DISPLAY_ENERGYOPTIONS=The display will be switched off after DISPLAY_INVERTING=Display inverting DISPLAY|ALARM_COUNT=Number alarm messages @@ -590,6 +734,16 @@ DISPLAY|UNIT|NONE=No unit DISPLAY|UNIT|PERCENT=Unit percentage DISPLAY|UNIT|WATT=Unit watt DISPLAY|WINDOW=Window symbol +DOOR_COMMAND|CLOSE=Closing the garage door +DOOR_COMMAND|NOP=No action +DOOR_COMMAND|OPEN=Opening the garage door +DOOR_COMMAND|PARTIAL_OPEN=Ventilation position +DOOR_COMMAND|STOP=Stop movement +DOOR_RECEIVER=Door-/garage door opener +DOOR_STATE|CLOSED=Position closed +DOOR_STATE|OPEN=Position opened +DOOR_STATE|POSITION_UNKNOWN=Position unknown +DOOR_STATE|VENTILATION_POSITION=Ventilation position DUAL_WHITE_BRIGHTNESS=Dual White Controller (dimmer) DUAL_WHITE_COLOR=Dual White Controller (colour) DUAL_WHITE_COLOR|LEVEL=Colour value @@ -598,6 +752,7 @@ DUAL_WHITE_COLOR|OLD_LEVEL=Last value DUAL_WHITE_COLOR|RAMP_STOP=Stop colour change DUAL_WHITE_COLOR|RAMP_TIME=Ramp time for colour change DURATION_UNIT=Unit duration +DURATION_UNIT|10MS=Unit duration: 10 mS DURATION_UNIT|D=Unit duration: Days DURATION_UNIT|H=Unit duration: Hours DURATION_UNIT|M=Unit duration: Minutes @@ -610,29 +765,67 @@ EMERGENCY_OPERATION|FALSE=Connection with room control unit OK EMERGENCY_OPERATION|TRUE=Connection failure with room control unit ENABLE_ROUTING=Routing active ENERGIE_METER_TRANSMITTER|AVERAGING=Averaging via -ENERGIE_METER_TRANSMITTER|CURRENT=Current ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER=Energy counter device ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER_OVERFLOW|FALSE=no transfer ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER_OVERFLOW|TRUE=Transfer ENERGIE_METER_TRANSMITTER|FREQUENCY=Frequency ENERGIE_METER_TRANSMITTER|POWER=Power ENERGIE_METER_TRANSMITTER|TX_THRESHOLD_POWER=TX Difference Power -ENERGIE_METER_TRANSMITTER|VOLTAGE=Voltage ERROR=Error +ERROR_BAD_RECHARGEABLE_BATTERY_HEALTH=Battery status: Not OK +ERROR_BAD_RECHARGEABLE_BATTERY_HEALTH|FALSE=Battery status: OK +ERROR_BAD_RECHARGEABLE_BATTERY_HEALTH|TRUE=Battery status: Not OK +ERROR_BUS_CONFIG_MISMATCH=The actual Bus topology is different to the configured Bus topology. +ERROR_BUS_CONFIG_MISMATCH|FALSE=The actual bus topology corresponds to the configured bus topology. +ERROR_BUS_CONFIG_MISMATCH|TRUE=The actual Bus topology is different to the configured Bus topology. ERROR_CODE=Error code +ERROR_COPROCESSOR=The channel is not accessible. Please check the power supply of the channel or deactivate it in the WebUI. +ERROR_COPROCESSOR|FALSE=Fehler CoProcessor: Nein +ERROR_COPROCESSOR|TRUE=Fehler CoProcessor: Ja +ERROR_NON_FLAT_POSITIONING=Error position detection +ERROR_NON_FLAT_POSITIONING|FALSE=Angle for position detection exceeded: No +ERROR_NON_FLAT_POSITIONING|TRUE=Angle for position detection exceeded: Yes +ERROR_OVERHEAT=Overheat: Yes ERROR_OVERHEAT|FALSE=Overheat: No ERROR_OVERHEAT|TRUE=Overheat: Yes +ERROR_OVERLOAD=Current overload: Yes ERROR_OVERLOAD|FALSE=Current overload: No ERROR_OVERLOAD|TRUE=Current overload: Yes +ERROR_POWER_FAILURE=Power supply error +ERROR_POWER_FAILURE|FALSE=Power supply OK +ERROR_POWER_FAILURE|TRUE=Power supply error +ERROR_POWER_SHORT_CIRCUIT_BUS_1=A short circuit between the power lines of Bus 1 was detected. +ERROR_POWER_SHORT_CIRCUIT_BUS_1|FALSE=No short circuit between the power lines of Bus 1 detected. +ERROR_POWER_SHORT_CIRCUIT_BUS_1|TRUE=A short circuit between the power lines of Bus 1 was detected. +ERROR_POWER_SHORT_CIRCUIT_BUS_2=A short circuit between the power lines of Bus 2 was detected. +ERROR_POWER_SHORT_CIRCUIT_BUS_2|FALSE=No short circuit between the power lines of Bus 2 detected. +ERROR_POWER_SHORT_CIRCUIT_BUS_2|TRUE=A short circuit between the power lines of Bus 2 was detected. ERROR_POWER|FALSE=Power supply error ERROR_POWER|TRUE=Power supply voltage OK ERROR_REDUCED|FALSE=Full power possible ERROR_REDUCED|TRUE=reduced power +ERROR_RESTART_NEEDED=The device must be restarted. +ERROR_RESTART_NEEDED|FALSE=Neustart nötig: Nein +ERROR_RESTART_NEEDED|TRUE=Neustart nötig: Ja ERROR_SABOTAGE|FALSE=Sabotage triggered ERROR_SABOTAGE|TRUE=Sabotage OK +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_1=A short circuit between 24V line and the Data line A and/or B of Bus 1 was detected. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_1|FALSE=No short circuit between 24V line and the Data line A and/or B of Bus 1 detected. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_1|TRUE=A short circuit between 24V line and the Data line A and/or B of Bus 1 was detected. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_2=A short circuit between 24V line and the Data line A and/or B of Bus 2 was detected. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_2|FALSE=No short circuit between 24V line and the Data line A and/or B of Bus 2 detected. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_2|TRUE=A short circuit between 24V line and the Data line A and/or B of Bus 2 was detected. +ERROR_UNDERVOLTAGE=Operating voltage not OK +ERROR_UNDERVOLTAGE|FALSE=Operating voltage OK +ERROR_UNDERVOLTAGE|TRUE=Operating voltage not OK ERROR_UPDATE|FALSE=Error device update: no ERROR_UPDATE|TRUE=Error device update: Yes +ERROR_WIND_COMMUNICATION|FALSE=Sensor wind directionCommunication OK +ERROR_WIND_COMMUNICATION|TRUE=Sensor wind directionCommunication error +ERROR_WIND_NORTH|FALSE=Sensor wind directionNorth direction calibrated +ERROR_WIND_NORTH|TRUE=Sensor wind directionNorth direction not calibrated ERROR|NO_ERROR=No error +EVENT_DELAYTIME=Message delay EVENT_DELAY_UNIT=Unit of event delay EVENT_DELAY_VALUE=Value event delay EVENT_FILTER_NUMBER=Sensitivity @@ -642,7 +835,16 @@ EVENT_RANDOMTIME_VALUE=Status messages random part EXPECT_AES=AES encryption EXTERNAL_CLOCK|FALSE=Energy-saving temperature mode inactive EXTERNAL_CLOCK|TRUE=Energy-saving temperature mode active +FREQUENCY_ALTERNATING_LOW_HIGH=Frequency low/high +FREQUENCY_ALTERNATING_LOW_MID_HIGH=Frequency low/average/high +FREQUENCY_FALLING=Frequency dropping +FREQUENCY_HIGHON_LONGOFF=Frequency high on, long off +FREQUENCY_HIGHON_OFF=Frequency high on/off FREQUENCY_INPUT=Analog +FREQUENCY_LOWON_LONGOFF_HIGHON_LONGOFF=Frequency low on - long off, high on - long off +FREQUENCY_LOWON_OFF_HIGHON_OFF=Frequency low on/off, high on/off +FREQUENCY_RISING=Frequency increasing +FREQUENCY_RISING_AND_FALLING=Frequency increasing/dropping FROST_PROTECTION_TEMPERATURE=Frost protection temperature FROST_PROTECTION|FALSE=Frost protection inactive FROST_PROTECTION|TRUE=Frost protection active @@ -657,7 +859,7 @@ HEATING_CLIMATECONTROL_TRANSCEIVER|ACTIVE_PROFILE=Active profile HEATING_CLIMATECONTROL_TRANSCEIVER|CONTROL_MODE=Auto/manu/party mode HEATING_CLIMATECONTROL_TRANSCEIVER|FROST_PROTECTION|FALSE=Frost protection inactive HEATING_CLIMATECONTROL_TRANSCEIVER|FROST_PROTECTION|TRUE=Frost protection active -HEATING_CLIMATECONTROL_TRANSCEIVER|HUMIDITY=Humidity +HEATING_CLIMATECONTROL_TRANSCEIVER|HUMIDITY=Relative humidity HEATING_CLIMATECONTROL_TRANSCEIVER|LEVEL=Valve opening HEATING_CLIMATECONTROL_TRANSCEIVER|PARTY_MODE|FALSE=Holiday mode inactive HEATING_CLIMATECONTROL_TRANSCEIVER|PARTY_MODE|TRUE=Holiday mode active @@ -673,11 +875,18 @@ HEATING_KEY_RECEIVER=Receiver thermostat HEATING_ROOM_TH_RECEIVER=Receiver thermostat HEATING_ROOM_TH_TRANSCEIVER=Transmitter thermostat HEATING_SHUTTER_CONTACT_RECEIVER=Receiver thermostat +HIGHEST_ILLUMINATION=Maximum level of brightness HUMIDITY=Relative humidity HUMIDITY_ALARM|FALSE=Humidity not exceeded HUMIDITY_ALARM|TRUE=Humidity exceeded HUMIDITY_LIMITER|FALSE=Operating mode humidity limit inactive HUMIDITY_LIMITER|TRUE=Operating mode humidity limit active +IDENTIFICATION_MODE_KEY_VISUAL|FALSE=Lighting system button: OFF +IDENTIFICATION_MODE_KEY_VISUAL|TRUE=Lighting system button: ON +IDENTIFICATION_MODE_LCD_BACKLIGHT|FALSE=Lighting Display: OFF +IDENTIFICATION_MODE_LCD_BACKLIGHT|TRUE=Lighting Display: ON +IDENTIFY_DURATION=Duration of lighting +IDENTIFY_TARGET_LEVEL=Brightness value of lighting ILLUMINATION=Brightness INHIBIT=Lock INHIBIT|FALSE=Lock inactive @@ -732,6 +941,11 @@ KEYMATIC|STATE|FALSE=Lock locked KEYMATIC|STATE|TRUE=Lock unlocked KEYPRESS_SIGNAL=Button sound KEY_TRANSCEIVER=Push button +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE=Channel behaviour +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE|BINARY_BEHAVIOR=Contact +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE|INACTIVE=not active +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE|KEY_BEHAVIOR=Button +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE|SWITCH_BEHAVIOR=Switch KEY_TRANSCEIVER|DBL_PRESS_TIME=Double-click time (keypad lock) KEY_TRANSCEIVER|LONG_PRESS_TIME=Minimum duration for long button press KEY|CHANNEL_FUNCTION=Channel function @@ -763,7 +977,10 @@ KEY|TEXTLINE_2=Text line LANGUAGE=Language LANGUAGE|ENGLISH=English LANGUAGE|GERMAN=German +LAST_PASSAGE_DIRECTION|FALSE=Last passage detected: No +LAST_PASSAGE_DIRECTION|TRUE=Last passage detected: Yes LED_DISABLE_CHANNELSTATE=Deactivate device LED +LED_DISABLE_LED_DISABLE_SENDSTATE=Deactivate visual confirmation of button press LED_ONTIME=LED on time (gn/rd) LED_SLEEP_MODE|OFF=Wake up display from standby mode LED_SLEEP_MODE|ON=Bring system to standby mode @@ -771,8 +988,8 @@ LED_STATUS|GREEN=Display green LED_STATUS|OFF=Display off LED_STATUS|ORANGE=Display orange LED_STATUS|RED=Display red -LEVEL=Dimming value -LEVEL_REAL=Dimming value real channel +LEVEL=Value +LEVEL_REAL=Value LIVE_MODE_RX=Live mode LOCAL_RESET_DISABLE=Lock reset via device button LOCAL_RESET_DISABLED=Lock reset via device button @@ -802,9 +1019,15 @@ LOGIC_PLUSINVERS=PLUS_INVERS (PLUS with previous inverting of level) LOGIC_XOR=XOR (equal to OR, but if both levels > 0. the result is 0) LOWBAT_SIGNAL=Low bat. signal LOWERING_MODE=Reduction temperature +LOWEST_ILLUMINATION=Minimum level of brightness MAINS_POWERED=Mains operation MAINTENANCE|ERROR_OVERHEAT=Overheat: Yes +MAINTENANCE|ON_MIN_LEVEL=Valve position changeover value +MAINTENANCE|PWM_AT_LOW_VALVE_POSITION=Automatic changeover from continuous to PWM (with small valve positions) MANU_MODE=Manu mode +MASS_CONCENTRATION_PM_1=Mass concentration PM1.0 +MASS_CONCENTRATION_PM_10=Mass concentration PM10 +MASS_CONCENTRATION_PM_2_5=Mass concentration PM2.5 MIN_MAX_VALUE_NOT_RELEVANT_FOR_MANU_MODE=Ignore min./max. temperature in manu mode MIOB_DIN_CONFIG=Digital input mode MODUS_BUTTON_LOCK=Mode operating lock @@ -827,6 +1050,8 @@ MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|NEW_DATA_SEND_IMMEDIATELY_DEF MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|NEW_DATA_STABLE_FOR_TIME_DEFAULT_DISABLE=Mode 6 MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|NEW_DATA_STABLE_FOR_TIME_DEFAULT_ENABLE=Mode 4 MOD_EM8BIT_TRANSMITTER|STATE=Value of data input +MOISTURE_DETECTED|FALSE=Humidity detected: No +MOISTURE_DETECTED|TRUE=Water level detected: Yes MOTIONDETECTOR_TRANSCEIVER=Motion detector MOTION_ACTIVE_TIME=Time after which the detected movement is reset MOTION_DETECTION_ACTIVE|FALSE=Motion detector inactive @@ -837,11 +1062,19 @@ MOTION_DETECTOR|ERROR|SABOTAGE=Sabotage MOTION_DETECTOR|MIN_INTERVAL=Minimum transmission interval MOTION|FALSE=no motion MOTION|TRUE=motion detected +MULTICAST_ROUTER_MODULE_ENABLED=MultiCast Routing +MULTI_MODE_INPUT_TRANSMITTER=Input Module NOT_USED=Unused +NUMBER_CONCENTRATION_PM_1=Quantity concentration PM1.0 +NUMBER_CONCENTRATION_PM_10=Quantity concentration PM10 +NUMBER_CONCENTRATION_PM_2_5=Quantity concentration PM2.5 OLD_LEVEL=Last dimming value ON_TIME=Switch-on time -OPERATING_VOLTAGE=Operating voltage in V: -OPTICAL_ALARM_SELECTION|BLINKING_ALTERNATELY_REPEATING=Alternating, slow flashing +OPERATING_VOLTAGE=Operating voltage in V +OPERATING_VOLTAGE_STATUS=Operating voltage in V +OPTICAL_ALARM_ACTIVE|FALSE=Visual signal deactivated +OPTICAL_ALARM_ACTIVE|TRUE=Visual signal activated +OPTICAL_ALARM_SELECTION|BLINKING_ALTERNATELY_REPEATING=Alternating slow flashing OPTICAL_ALARM_SELECTION|BLINKING_BOTH_REPEATING=Simultaneous slow flashing OPTICAL_ALARM_SELECTION|CONFIRMATION_SIGNAL_0=Confirmation signal 0 - long long OPTICAL_ALARM_SELECTION|CONFIRMATION_SIGNAL_1=Confirmation signal 1 - long short @@ -857,10 +1090,26 @@ PARAM_SELECT|T1-T2=Difference temperature sensor 1 - sensor 2 PARAM_SELECT|T2=Temperature sensor 2 PARAM_SELECT|T2-T1=Difference temperature sensor 2 - sensor 1 PARTY_SET_POINT_TEMPERATURE=Party/holiday temperature +PARTY_START_DAY=Holiday start day +PARTY_START_MONTH=Holiday start month +PARTY_START_TIME=Holiday start time +PARTY_START_YEAR=Holiday start year +PARTY_STOP_DAY=Holiday end day +PARTY_STOP_MONTH=Holiday end month +PARTY_STOP_TIME=Holiday end time +PARTY_STOP_YEAR=Holiday end year +PARTY_TEMPERATURE=Holiday temperature PARTY_TIME_END=Party/holiday end time PARTY_TIME_START=Party/holiday start time +PASSAGE_COUNTER_OVERFLOW|FALSE=Passage counter overrun: No +PASSAGE_COUNTER_OVERFLOW|TRUE=Passage counter overrun: Yes +PASSAGE_COUNTER_VALUE=Number of passages +PASSAGE_DETECTOR_COUNTER_TRANSMITTER=Passage sensor +PASSAGE_DETECTOR_COUNTER_TRANSMITTER|CHANNEL_OPERATION_MODE=Operating mode +PASSAGE_DETECTOR_DIRECTION_TRANSMITTER=Direction recognition PEER_NEEDS_BURST=Burst signal necessary PIR_OPERATION_MODE=Normal / eco mode +PIR_SENSITIVITY=Sensor sensitivity POSITION_SAVE_TIME=Position saving time POWERMETER_IEC1|ENERGY_COUNTER=Energy counter device POWERMETER_IEC1|GAS_ENERGY_COUNTER=Energy and gas metering device @@ -888,19 +1137,28 @@ POWERMETER|VOLTAGE=Voltage POWERUP_ACTION=Activity on power supply POWERUP_JUMPTARGET=Activity on power supply POWERUP_OFF=none +POWERUP_OFFDELAY_VALUE=Value switch off delay +POWERUP_OFFTIME_UNIT=Unit of switch-off time POWERUP_ON=simulate short button press POWERUP_ONDELAY_UNIT=Unit of switch on delay POWERUP_ONDELAY_VALUE=Value switch on delay POWERUP_ONTIME_UNIT=Unit of switch-on time POWERUP_ONTIME_VALUE=Value switch-on time +POWER_MAINS_FAILURE|FALSE=Power failure: No +POWER_MAINS_FAILURE|TRUE=Power failure: Yes POWER_SUPPLY=Power supply +PRESENCEDETECTOR_TRANSCEIVER=Presence Sensor PRESENCEDETECTOR_TRANSCEIVER|MIN_INTERVAL=Minimum transmission interval +PRESENCE_DETECTION_ACTIVE|FALSE=Presence detector not active +PRESENCE_DETECTION_ACTIVE|TRUE=Presence detector active +PRESENCE_DETECTION_STATE|FALSE=No presence detected +PRESENCE_DETECTION_STATE|TRUE=Presence detected PRESS_LONG=Button press long PRESS_LONG|TRUE=Button press long PRESS_SHORT=Button press short PRESS_SHORT|TRUE=Button press short -PROCESS|NOT_STABLE=Time program: Active -PROCESS|STABLE=Time program: Inactive +PROCESS|NOT_STABLE=Device active +PROCESS|STABLE=Device not active PULSE_SENSOR=Pulse sensor PULSE_SENSOR|SEQUENCE_OK=Activated PULSE_SENSOR|SEQUENCE_PULSE_1=Level 1 in s @@ -914,6 +1172,7 @@ PULSE_SENSOR|SEQUENCE_PULSE_4|NOT_USED=Unused PULSE_SENSOR|SEQUENCE_PULSE_5=Level 3 in s PULSE_SENSOR|SEQUENCE_PULSE_5|NOT_USED=Unused PULSE_SENSOR|SEQUENCE_TOLERANCE=Tolerance in s +RADIATOR_THERMOSTAT=Radiator Thermostat RAINDETECTOR=Rain sensor RAINDETECTOR|COND_TX_THRESHOLD_HI=Detection threshold for dry conditions RAINDETECTOR|COND_TX_THRESHOLD_LO=Detection threshold for rain @@ -922,13 +1181,35 @@ RAINDETECTOR|EVENT_RELEASE_FILTER_TIME=Filter time for dryness detection RAINDETECTOR|STATE_HIGH_HOLD_TIME=Time until next measurement when rain is detected RAINDETECTOR|STATE|DRY=Dry conditions RAINDETECTOR|STATE|RAIN=Rain +RAINING=Rain +RAINING|FALSE=currently not raining +RAINING|TRUE=currently raining +RAIN_COUNTER=Rainfall +RAIN_COUNTER_OVERFLOW|FALSE=Overrun rainfall counter: No +RAIN_COUNTER_OVERFLOW|TRUE=Overrun rainfall counter: Yes +RAIN_DETECTION_TRANSMITTER=Rain sensor RAMP_STOP=Stop dimming ramp RAMP_TIME=Dimming time +RAMP_TIME_UNIT=Unit ramp time +RAMP_TIME_UNIT|10MS=Unit ramp time: mS +RAMP_TIME_UNIT|D=Unit ramp time: Days +RAMP_TIME_UNIT|H=Unit ramp time: Hours +RAMP_TIME_UNIT|M=Unit ramp time: Minutes +RAMP_TIME_UNIT|S=Unit ramp time: Seconds +RAMP_TIME_VALUE=Value ramp time REDUCE_LEVEL=Reducing level over temperature REDUCE_TEMP_LEVEL=Reducing threshold over temperature +REFERENCE_RUNNING_TIME_BOTTOM_TOP_UNIT=Unit movement time +REFERENCE_RUNNING_TIME_BOTTOM_TOP_VALUE=Value movement time +REFERENCE_RUNNING_TIME_SLATS_UNIT=Unit slat adjustment time +REFERENCE_RUNNING_TIME_SLATS_VALUE=Value slat adjustment time +REFERENCE_RUNNING_TIME_TOP_BOTTOM_UNIT=Unit movement time +REFERENCE_RUNNING_TIME_TOP_BOTTOM_VALUE=Value movement time REMOTECONTROL_RECEIVER=Radiator Thermostat (Receiver remote control) REPEATED_LONG_PRESS_TIMEOUT_UNIT=Unit for time-out REPEATED_LONG_PRESS_TIMEOUT_VALUE=Value for time-out +RESET_MOTION=Reset status +RESET_PRESENCE=Reset status RESTART_BUTTONPRESS=simulate short button press RESTART_BUTTONPRESS_IF_WAS_ON=simulate short button press, if switched on before RESTART_LAST=restore previous status @@ -943,9 +1224,9 @@ RGBW_COLOR|COLOR=Color value RGBW_COLOR|WHITE_ADJUSTMENT_VALUE_BLUE=White balance blue RGBW_COLOR|WHITE_ADJUSTMENT_VALUE_GREEN=White balance green RGBW_COLOR|WHITE_ADJUSTMENT_VALUE_RED=White balance red +ROTARY_CONTROL_TRANSCEIVER=Rotary Button ROTARY_HANDLE_SENSOR=Window twist-handle contact ROTARY_HANDLE_SENSOR|ERROR|SABOTAGE=Sabotage -ROTARY_HANDLE_SENSOR|EVENT_DELAYTIME=Message delay ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A=Message in position down ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A|CLOSED=closed ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A|NO_MSG=no message @@ -983,10 +1264,17 @@ ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_C|TILTED=tilted ROTARY_HANDLE_TRANSCEIVER|STATE|CLOSED=Window status: locked ROTARY_HANDLE_TRANSCEIVER|STATE|OPEN=Window status: open ROTARY_HANDLE_TRANSCEIVER|STATE|TILTED=Window status: tilted +ROUTER_MODULE_ENABLED=Device serves as router RSSI_DEVICE=RSSI device RSSI_PEER=RSSI peer SABOTAGE_MSG=Sabotage message SECTION=Profile section: +SECTION_STATUS|NORMAL=Status Section: Normal +SECTION_STATUS|UNKNOWN=Status Section: Unknown +SELF_CALIBRATION_RESULT|FALSE=Calibration run not required +SELF_CALIBRATION_RESULT|TRUE=Teach-in procedure successful +SELF_CALIBRATION|START=Start calibration run +SELF_CALIBRATION|STOP=End calibration run SENSOR_ERROR|FALSE=Sensor OK SENSOR_ERROR|TRUE=Sensor disturbed SENSOR_FOR_CARBON_DIOXIDE=Air quality sensor @@ -1012,6 +1300,7 @@ SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D|NO_MSG=no message SENSOR_FOR_CARBON_DIOXIDE|STATE|LEVEL_ADDED=CO2 concentration increased SENSOR_FOR_CARBON_DIOXIDE|STATE|LEVEL_ADDED_STRONG=CO2 concentration greatly increased SENSOR_FOR_CARBON_DIOXIDE|STATE|LEVEL_NORMAL=CO2 concentration normal +SENSOR_SENSITIVITY=Sensor sensitivity SENSOR|FALSE=closed SENSOR|INPUT_LOCKED=Input locked SENSOR|TRUE=open @@ -1043,10 +1332,23 @@ SHUTTER_CONTACT|STATE|CLOSED=closed SHUTTER_CONTACT|STATE|FALSE=closed SHUTTER_CONTACT|STATE|OPEN=open SHUTTER_CONTACT|STATE|TRUE=open +SHUTTER_TRANSMITTER|ACTIVITY_STATE|DOWN=Shutter moves down +SHUTTER_TRANSMITTER|ACTIVITY_STATE|STABLE=Shutter not moving +SHUTTER_TRANSMITTER|ACTIVITY_STATE|UNKNOWN=Shutter activity unknown +SHUTTER_TRANSMITTER|ACTIVITY_STATE|UP=Shutter moves up SHUTTER_TRANSMITTER|LEVEL=Blind level SHUTTER_TRANSMITTER|LEVEL_2=Slats position +SHUTTER_TRANSMITTER|PROCESS|NOT_STABLE=Shutter moves +SHUTTER_TRANSMITTER|PROCESS|STABLE=Shutter not moving +SHUTTER_VIRTUAL_RECEIVER=Shutter actuator +SHUTTER_VIRTUAL_RECEIVER|ACTIVITY_STATE|DOWN=Shutter moves down +SHUTTER_VIRTUAL_RECEIVER|ACTIVITY_STATE|STABLE=Shutter not moving +SHUTTER_VIRTUAL_RECEIVER|ACTIVITY_STATE|UNKNOWN=Shutter activity unknown +SHUTTER_VIRTUAL_RECEIVER|ACTIVITY_STATE|UP=Shutter moves up SHUTTER_VIRTUAL_RECEIVER|LEVEL=Blind level SHUTTER_VIRTUAL_RECEIVER|LEVEL_2=Slats position +SHUTTER_VIRTUAL_RECEIVER|PROCESS|NOT_STABLE=Shutter moves +SHUTTER_VIRTUAL_RECEIVER|PROCESS|STABLE=Shutter not moving SHUTTER_VIRTUAL_RECEIVER|STOP=Stop SIGNAL=Confirmation signal SIGNAL_CHIME=Signal actuator (acoustically) @@ -1066,6 +1368,7 @@ SIGNAL_TONE|HIGH=high SIGNAL_TONE|LOW=low SIGNAL_TONE|MID=mid SIGNAL_TONE|VERY_HIGH=very high +SIMPLE_SWITCH_RECEIVER=Switch actor SMOKE_DETECTOR_ALARM_STATUS|IDLE_OFF=Standby SMOKE_DETECTOR_ALARM_STATUS|INTRUSION_ALARM=Burglar alarm SMOKE_DETECTOR_ALARM_STATUS|PRIMARY_ALARM=Local alarm @@ -1102,6 +1405,10 @@ SMOKE_DETECTOR|REPEAT_ENABLE=Forwarding of received data telegrams SMOKE_DETECTOR|STATE|FALSE=No smoke detected SMOKE_DETECTOR|STATE|TRUE=Smoke detected SOFTONOFF=Soft On/Off +SOUNDFILE|DO_NOT_CARE=Proceed with current title +SOUNDFILE|INTERNAL_SOUNDFILE=Internal device sound +SOUNDFILE|OLD_VALUE=Title last played +SOUNDFILE|RANDOM_SOUNDFILE=Shuffle SOUND_ID=Alarm signal SOUND_LONG=Long SOUND_LONG_LONG=Long / Long @@ -1112,6 +1419,7 @@ SOUND_SHORT=Short SOUND_SHORT_SHORT=Short / Short SPEED_MULTIPLIER=Factor PWM frequency STANDBY_TIME=Time until standby mode +STATE_RESET_RECEIVER=Suppression of motion detection STATE|FALSE=Switching status: Off STATE|TRUE=Switching status: On STATUSINFO_MINDELAY=Status messages minimum delay time @@ -1123,8 +1431,20 @@ STATUS_INDICATOR|ON_TIME=Switch-on time STATUS_INDICATOR|STATE|FALSE=Switching status off STATUS_INDICATOR|STATE|TRUE=Switching status on STATUS_MESSAGE_TEXT_ALIGNMENT_LEFT_ALIGNED=Range status message left +STICKY_UNREACH|FALSE=Device communication was disturbed: No +STICKY_UNREACH|TRUE=Device communication was disturbed: Yes +STORM_LOWER_THRESHOLD=Wind alert off threshold +STORM_UPPER_THRESHOLD=Wind alert on threshold SUBMIT=Channel action +SUNSHINEDURATION=Sunshine duration +SUNSHINEDURATION_OVERFLOW|FALSE=Overrun counter sunshine: No +SUNSHINEDURATION_OVERFLOW|TRUE=Overrun counter sunshine: Yes +SUNSHINE_THRESHOLD=Sunshine threshold +SUNSHINE_THRESHOLD_OVERRUN=Sunshine +SUNSHINE_THRESHOLD_OVERRUN|FALSE=currently no sunshine +SUNSHINE_THRESHOLD_OVERRUN|TRUE=currently sunshine SWITCH=Switch actuator +SWITCH_ACTUATOR=Switch actuator SWITCH_INTERFACE=Switch interface SWITCH_INTERFACE|PRESS=Activated SWITCH_INTERFACE|STATE|FALSE=Switch position: pressed down @@ -1141,6 +1461,7 @@ SWITCH|STATE|TRUE=Switching status: on SWITCH|STATUSINFO_RANDOM_A=To avoid collisions during transmission of + TACTILE_SWITCH|FALSE=Operating mode push-button inactive TACTILE_SWITCH|TRUE=Operating mode push-button active +TEMPERATURE=Temperature TEMPERATURE_COMFORT=Comfort temperature TEMPERATURE_LIMITER|FALSE=Operating mode temperature limit inactive TEMPERATURE_LIMITER|TRUE=Operating mode temperature limit active @@ -1148,6 +1469,10 @@ TEMPERATURE_LOWERING=Eco temperature TEMPERATURE_MAXIMUM=Maximum temperature TEMPERATURE_MINIMUM=Minimum temperature TEMPERATURE_OFFSET=Temperature offset +TEMPERATURE_OUT_OF_RANGE|FALSE=Ambient temperature OK +TEMPERATURE_OUT_OF_RANGE|TRUE=Ambient temperature invalid +TEMP_HUMIDITY_PARTICULATE_MATTER_TRANSMITTER|INTERVAL_UNIT=Unit of automatic sensor cleaning +TEMP_HUMIDITY_PARTICULATE_MATTER_TRANSMITTER|INTERVAL_VALUE=Value of automatic sensor cleaning THERMALCONTROL_TRANSMIT=Temperature sensor room thermostat TILT_SENSOR=Tilt sensor TILT_SENSOR|EVENT_FILTERTIME=Filter time @@ -1168,10 +1493,12 @@ TX_MINDELAY=Minimum transmission interval TX_MINDELAY_UNIT=Unit of minimum transmission interval TX_MINDELAY_VALUE=Value minimum transmission interval TX_THRESHOLD_POWER=TX Difference Power +TYPICAL_PARTICLE_SIZE=Typical particle size UNREACH|FALSE=Device communication OK UNREACH|TRUE=Device communication disturbed USER_COLOR=Channel action USER_PROGRAM=Channel action +VALVE_MAXIMUM_POSITION=maximum valve opening position VALVE_STATE=Valve position VALVE_STATE|ADAPTION_DONE=Adaption run performed VALVE_STATE|ADAPTION_IN_PROGRESS=Adaption run active @@ -1184,6 +1511,8 @@ VALVE_STATE|TOO_TIGHT=Valve sluggish/blocked VALVE_STATE|WAIT_FOR_ADAPTION=Waiting for adaption run VENT_CLOSED=Close valve VENT_OPEN=Open valve +VIR-LG-ONOFF-CH|LEVEL|FALSE=Switching status: Off +VIR-LG-ONOFF-CH|LEVEL|TRUE=Switching status: On VIRTUAL_DIMMER=Dimming actuator VIRTUAL_DIMMER|ERROR_OVERHEAT=Overheat VIRTUAL_DIMMER|ERROR_OVERLOAD=Overload @@ -1200,6 +1529,7 @@ VIRTUAL_DUAL_WHITE_COLOR|RAMP_STOP=Stop colour change VIRTUAL_DUAL_WHITE_COLOR|RAMP_TIME=Ramp time for colour change VIRTUAL_KEY=Virtual remote control VIRTUAL_KEY|LEVEL=Send percentage +VOLTAGE=Voltage VOLTAGE_0=Value (relative) for control voltage at 0% VOLTAGE_100=Value (relative) for control voltage at 100% VOLUME_0=Volume 0% @@ -1218,6 +1548,7 @@ WAKEUP_BEHAVIOUR_STATUS_MSG_CONFIRMATION=Button press evaluation status message WAKEUP_BEHAVIOUR_STATUS_MSG_RESISTANCE=Status message can be deleted only via CCU WAKEUP_BEHAVIOUR_STATUS_SIGNALIZATION_CONFIRMATION=Button press evaluation signalling WAKEUP_DEFAULT_CHANNEL=Initial channel when activating +WALLMOUNTED_THERMOSTAT=Wall Thermostat WATERDETECTIONSENSOR=Water detector WATERDETECTIONSENSOR|EVENT_FILTERTIME=Filter time WATERDETECTIONSENSOR|MSG_FOR_POS_A=Dry conditions @@ -1235,31 +1566,35 @@ WATERDETECTIONSENSOR|MSG_FOR_POS_C|WET=Humidity detected WATERDETECTIONSENSOR|STATE|DRY=Dry WATERDETECTIONSENSOR|STATE|WATER=Water level detected WATERDETECTIONSENSOR|STATE|WET=Humidity detected +WATERLEVEL_DETECTED|FALSE=Water level detected: No +WATERLEVEL_DETECTED|TRUE=Water level detected: Yes +WATER_DETECTION_TRANSMITTER=Water sensor +WATER_DETECTION_TRANSMITTER|ALARMSTATE|FALSE=Humidity or water level detected: No +WATER_DETECTION_TRANSMITTER|ALARMSTATE|TRUE=Humidity or water level detected: Yes WEATHER=Weather sensor WEATHER_RECEIVER=Radiator Thermostat (Receiver weather data) +WEATHER_TRANSMIT|ALARMSTATE|FALSE=Humidity or water level detected: No +WEATHER_TRANSMIT|ALARMSTATE|TRUE=Humidity or water level detected: Yes WEATHER_TRANSMIT|HUMIDITY=Relative humidity WEATHER_TRANSMIT|TEMPERATURE=Temperature -WEATHER|AIR_PRESSURE=Barometric pressure -WEATHER|BRIGHTNESS=Brightness -WEATHER|HUMIDITY=Relative humidity -WEATHER|RAINING=Rain -WEATHER|RAINING|FALSE=currently not raining -WEATHER|RAINING|TRUE=currently raining -WEATHER|RAIN_COUNTER=Rainfall -WEATHER|STORM_LOWER_THRESHOLD=Wind alert off threshold -WEATHER|STORM_UPPER_THRESHOLD=Wind alert on threshold -WEATHER|SUNSHINEDURATION=Sunshine duration -WEATHER|SUNSHINE_THRESHOLD=Sunshine threshold -WEATHER|TEMPERATURE=Temperature -WEATHER|WIND_DIRECTION=Wind direction -WEATHER|WIND_DIRECTION_RANGE=Wind direction fluctuation range -WEATHER|WIND_SPEED=Wind velocity -WEATHER|WIND_SPEED_RESULT_SOURCE=Type of wind velocity value -WEATHER|WIND_SPEED_RESULT_SOURCE|AVERAGE_VALUE=Average -WEATHER|WIND_SPEED_RESULT_SOURCE|MAX_VALUE=Maximum value +WEEK_PROGRAM_CHANNEL_LOCKS=Channels in auto mode +WEEK_PROGRAM_TARGET_CHANNEL_LOCKS=Channels for mode week program (binary) +WEEK_PROGRAM_TARGET_CHANNEL_LOCK|AUTO_MODE_WITHOUT_RESET=week program: Auto without reset +WEEK_PROGRAM_TARGET_CHANNEL_LOCK|AUTO_MODE_WITH_RESET=week program: Auto with reset (reset without function) +WEEK_PROGRAM_TARGET_CHANNEL_LOCK|MANU_MODE=week program: Manually WHITE=Colour temperature WINDOW_STATE=Window status WINDOW_SWITCH_RECEIVER=Radiator thermostat +WIND_DIR=Wind direction +WIND_DIRECTION=Wind direction +WIND_DIRECTION_RANGE=Wind direction fluctuation range +WIND_DIR_RANGE=Wind direction fluctuation range +WIND_SPEED=Wind velocity +WIND_SPEED_RESULT_SOURCE=Type of wind velocity value +WIND_SPEED_RESULT_SOURCE|AVERAGE_VALUE=Average +WIND_SPEED_RESULT_SOURCE|MAX_VALUE=Maximum value +WIND_THRESHOLD_OVERRUN|FALSE=Wind threshold not exceeded +WIND_THRESHOLD_OVERRUN|TRUE=Wind threshold exceeded WINMATIC=Window tilt actuator WINMATIC|ERROR|MOTOR_TILT_ERROR=Error tilt actuator WINMATIC|ERROR|MOTOR_TURN_ERROR=Error rotary actuator diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/generated-descriptions_de.properties b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/generated-descriptions_de.properties index cdbf0f0fb4f06..7baf1201caf83 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/generated-descriptions_de.properties +++ b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/generated-descriptions_de.properties @@ -1,50 +1,45 @@ -# -------------- generated descriptions Fri Jul 21 16:59:59 CEST 2017 -------------- +# -------------- generated descriptions Sun Apr 25 19:44:22 CEST 2021 -------------- # DON'T CHANGE THIS FILE! 263_130=Funk-Schaltaktor 1-fach, Unterputzmontage 263_131=Funk-Schaltaktor 1-fach, Unterputzmontage 263_132=Funk-Dimmaktor 1-fach, Phasenanschnitt, Zwischendeckenmontage -263_133=Funk-Dimmaktor 1-fach fr Markenschalter, Phasenabschnitt, Unterputzmontage +263_133=Funk-Dimmaktor 1-fach für Markenschalter, Phasenabschnitt, Unterputzmontage 263_134=Funk-Dimmaktor 2-fach, Phasenabschnitt, Aufputzmontage 263_135=Funk-Wandtaster 2-fach im 55er Rahmen 263_144=Funk-Schalterschnittstelle 3-fach, Unterputzmontage 263_145=Funk-Tasterschnittstelle 4-fach, Unterputzmontage 263_146=Funk-Rollladenaktor 1-fach, Unterputzmontage 263_147=Funk-Rollladenaktor 1-fach, Aufputzmontage -263_149_/_263_150=Schco WCS-TipTronic-Platine +263_149_/_263_150=Schüco WCS-TipTronic-Platine 263_155=Funk-Display-Wandtaster 2-fach, Aufputzmontage 263_157=Funk-Temperatursensor innen -263_158=Funk-Temperatur-/ Feuchtesensor auen +263_158=Funk-Temperatur-/ Feuchtesensor außen 263_160=Funk-Kohlendioxid-Sensor 263_162=Funk-Bewegungsmelder innen 263_167=Funk-Rauchmelder 263_167_Gruppe=Funk-Rauchmelder (Gruppe) -ALPHA-IP-RBG=Raumbediengert Display -ALPHA-IP-RBGa=Raumbediengert Analog -ASIR=Homematic IP Innensirene -BDT=Homematic IP Dimmaktor fr Markenschalter, Unterputzmontage +ALPHA-IP-RBG=Raumbediengerät Display +ALPHA-IP-RBGa=Raumbediengerät Analog +BDT=Homematic IP Dimmaktor für Markenschalter, Unterputzmontage BRC-H=Funk- Handsender DORMA, 4-Kanal BSM=Homematic IP Schaltaktor mit Leistungsmessung -DEVICE=Unbekanntes Gert -FAL230-C10=Homematic IP Fussbodenheizungsaktor -FAL230-C6=Homematic IP Fussbodenheizungsaktor -FAL24-C10=Homematic IP Fussbodenheizungsaktor -FAL24-C6=Homematic IP Fussbodenheizungsaktor +DEVICE=Unbekanntes Gerät FDT=Homematic IP Dimmaktor, Unterputzmontage FSM=Homematic IP Schaltaktor mit Leistungsmessung, Unterputzmontage FSM16=Homematic IP Schaltaktor mit Leistungsmessung, Unterputzmontage -HM-CC-RT-DN=Funk-Heizkrperthermostat +HM-CC-RT-DN=Funk-Heizkörperthermostat HM-CC-SCD=Funk-Kohlendioxid-Sensor HM-CC-TC=Funk-Wandthermostat HM-CC-VD=Funk-Stellantrieb HM-CC-VG-1=Gruppe Heizungssteuerung -HM-CCU-1=HomeMatic Zentrale +HM-CCU-1=Homematic Zentrale HM-DW-WM=Funk-Dimmaktor 2-fach PWM LED HM-Dis-EP-WM55=Display-Statusanzeige mit E-Paper-Display HM-Dis-TD-T=Funk-Statusanzeige HM-Dis-WM55=Display-Statusanzeige -HM-EM-CCM=Zhlersensor Kamera Modul -HM-EM-CMM=Zhlersensor Management Modul +HM-EM-CCM=Zählersensor Kamera Modul +HM-EM-CMM=Zählersensor Management Modul HM-ES-PMSw1-DR=Funk-Schaltaktor mit Leistungsmessung, Hutschienenmontage HM-ES-PMSw1-Pl=Funk-Schaltaktor mit Leistungsmessung HM-ES-PMSw1-Pl-DN-R1=Funk-Schaltaktor mit Leistungsmessung @@ -53,14 +48,14 @@ HM-ES-PMSw1-Pl-DN-R3=Funk-Schaltaktor mit Leistungsmessung HM-ES-PMSw1-Pl-DN-R4=Funk-Schaltaktor mit Leistungsmessung HM-ES-PMSw1-Pl-DN-R5=Funk-Schaltaktor mit Leistungsmessung HM-ES-PMSw1-SM=Funk-Schaltaktor mit Leistungsmessung -HM-ES-TX-WM=Funk-Sender fr Energiezhler-Sensor +HM-ES-TX-WM=Funk-Sender für Energiezähler-Sensor HM-LC-AO-SM=Funk 0-10V Aktor HM-LC-Bl1-FM=Funk-Rollladenaktor 1-fach, Unterputzmontage HM-LC-Bl1-PB-FM=Funk-Rollladenaktor 1-fach, Unterputzmontage mit Tasteraufsatz HM-LC-Bl1-SM=Funk-Rollladenaktor 1-fach, Aufputzmontage -HM-LC-Bl1PBU-FM=Funk-Rollladenaktor 1-fach fr Markenschalter, Unterputz -HM-LC-DDC1-PCB=Funk-Empfnger 1-Kanal -HM-LC-DW-WM=Funk-Controller fr Dual-White-LEDs +HM-LC-Bl1PBU-FM=Funk-Rollladenaktor 1-fach für Markenschalter, Unterputz +HM-LC-DDC1-PCB=Funk-Empfänger 1-Kanal +HM-LC-DW-WM=Funk-Controller für Dual-White-LEDs HM-LC-Dim1L-CV=Funk-Dimmaktor 1-fach, Phasenanschnitt, Zwischendeckenmontage HM-LC-Dim1L-Pl=Funk-Zwischenstecker-Dimmaktor 1-fach, Phasenanschnitt HM-LC-Dim1L-Pl-2=Funk-Zwischenstecker-Dimmaktor 1-fach, Phasenanschnitt @@ -72,11 +67,11 @@ HM-LC-Dim1T-FM=Funk-Dimmaktor 1-fach, Phasenabschnitt, Unterputzmontage HM-LC-Dim1T-Pl=Funk-Dimmaktor 1-fach, Zwischenstecker, Phasenabschnitt HM-LC-Dim1T-Pl-2=Funk-Dimmaktor 1-fach, Zwischenstecker, Phasenabschnitt HM-LC-Dim1T-Pl-3=Funk-Dimmaktor 1-fach, Zwischenstecker, Phasenabschnitt -HM-LC-Dim1TPBU-FM=Funk-Dimmaktor 1-fach fr Markenschalter, Phasenabschnitt, Unterputzmontage -HM-LC-Dim1TPBU-FM-2=Funk-Dimmaktor 1-fach fr Markenschalter, Phasenabschnitt, Unterputzmontage +HM-LC-Dim1TPBU-FM=Funk-Dimmaktor 1-fach für Markenschalter, Phasenabschnitt, Unterputzmontage +HM-LC-Dim1TPBU-FM-2=Funk-Dimmaktor 1-fach für Markenschalter, Phasenabschnitt, Unterputzmontage HM-LC-Dim2L-SM=Funk-Dimmaktor 2-fach, Phasenanschnitt, Aufputzmontage HM-LC-Dim2T-SM=Funk-Dimmaktor 2-fach, Phasenabschnitt, Aufputzmontage -HM-LC-Ja1PBU-FM=Funk-Jalousieaktor 1-fach fr Markenschalter, Unterputz +HM-LC-Ja1PBU-FM=Funk-Jalousieaktor 1-fach für Markenschalter, Unterputz HM-LC-Sw1-Ba-PCB=Funk-Schaltaktor 1-fach, Platine Batterie HM-LC-Sw1-DR=Funk-Schaltaktor 1-fach, Hutschienenmontage HM-LC-Sw1-FM=Funk-Schaltaktor 1-fach, Unterputzmontage @@ -93,7 +88,7 @@ HM-LC-Sw1-Pl-DN-R5=Funk-Schaltaktor 1-fach, Zwischenstecker HM-LC-Sw1-Pl-OM54=Funk-Schalter, 1-Kanal HM-LC-Sw1-SM=Funk-Schaltaktor 1-fach, Aufputzmontage HM-LC-Sw1-SM-ATmega168=Funk-Schaltaktor 1-fach, Aufputzmontage -HM-LC-Sw1PBU-FM=Funk-Schaltaktor 1-fach fr Markenschalter, Unterputzmontage +HM-LC-Sw1PBU-FM=Funk-Schaltaktor 1-fach für Markenschalter, Unterputzmontage HM-LC-Sw2-DR=Funk-Schaltaktor 2-fach, Hutschienenmontage HM-LC-Sw2-FM=Funk-Schaltaktor 2-fach, Unterputzmontage HM-LC-Sw2-PB-FM=Funk-Schaltaktor 2-fach, Unterputzmontage @@ -107,9 +102,9 @@ HM-LC-Sw4-WM=Funk-Schaltaktor 4-fach, Wandmontage HM-MOD-EM-8=Funk-Sendemodul 8-Kanal, Platine Batterie HM-MOD-EM-8Bit=Funk-Sendemodul, 8-Bit HM-MOD-Re-8=Funk-Schaltaktor 8-fach, Platine Batterie -HM-OU-CF-Pl=Funk-Trgong mit Signalleuchte +HM-OU-CF-Pl=Funk-Türgong mit Signalleuchte HM-OU-CFM-Pl=MP3 Funk-Gong mit Signalleuchte -HM-OU-CFM-TW=MP3 Funk-Gong mit Signalleuchte fr Batteriebetrieb +HM-OU-CFM-TW=MP3 Funk-Gong mit Signalleuchte für Batteriebetrieb HM-OU-CM-PCB=Funk-Gongmodul MP3 mit Speicher HM-OU-LED16=Funk-Statusanzeige LED 16 HM-PB-2-FM=Funk-Wandtaster 2-fach @@ -124,26 +119,26 @@ HM-RC-12-B=Funk-Fernbedienung 12 Tasten, schwarz HM-RC-19=Funk-Fernbedienung 19 Tasten HM-RC-19-B=Funk-Fernbedienung 19 Tasten HM-RC-19-SW=Funk-Fernbedienung 19 Tasten -HM-RC-2-PBU-FM=Funk-Sender 2-fach fr Markenschalter, Unterputzmontage +HM-RC-2-PBU-FM=Funk-Sender 2-fach für Markenschalter, Unterputzmontage HM-RC-4=Funk-Handsender 4 Tasten HM-RC-4-B=Funk-Handsender 4 Tasten HM-RC-8=Funk-Handsender 8 Tasten HM-RC-Dis-H-x-EU=Funk-Fernbedienung mit Display -HM-RC-Key3=Funk-Handsender fr KeyMatic -HM-RC-Key3-B=Funk-Handsender fr KeyMatic +HM-RC-Key3=Funk-Handsender für KeyMatic +HM-RC-Key3-B=Funk-Handsender für KeyMatic HM-RC-P1=Funk-Panikhandsender -HM-RC-Sec3=Funk-Handsender fr Alarmzentrale -HM-RC-Sec3-B=Funk-Handsender fr Alarmzentrale +HM-RC-Sec3=Funk-Handsender für Alarmzentrale +HM-RC-Sec3-B=Funk-Handsender für Alarmzentrale HM-RCV-50=Virtuelle Fernbedienung (drahtlos) -HM-SCI-3-FM=Funk-Schlieerkontaktschnittstelle 3-fach, Unterputzmontage +HM-SCI-3-FM=Funk-Schließerkontaktschnittstelle 3-fach, Unterputzmontage HM-Sec-Key=KeyMatic HM-Sec-Key-O=KeyMatic HM-Sec-Key-S=KeyMatic HM-Sec-MDIR=Funk-Bewegungsmelder innen HM-Sec-RHS=Funk-Fenster-/ Drehgriffkontakt -HM-Sec-SC=Funk-Tr-/ Fensterkontakt -HM-Sec-SC-2=Funk-Tr-/ Fensterkontakt -HM-Sec-SCo=Funk- Tr-/Fensterkontakt optisch +HM-Sec-SC=Funk-Tür-/ Fensterkontakt +HM-Sec-SC-2=Funk-Tür-/ Fensterkontakt +HM-Sec-SCo=Funk- Tür-/Fensterkontakt optisch HM-Sec-SD=Funk-Rauchmelder HM-Sec-SD-Team=Funk-Rauchmelder (Gruppe) HM-Sec-SFA-SM=Funk-Sirenen-/Blitzansteuerung @@ -153,51 +148,132 @@ HM-Sec-WDS=Funk-Wassermelder HM-Sec-WDS-2=Funk-Wassermelder HM-Sec-Win=WinMatic HM-Sen-DB-PCB=Funk-Klingelsignalsensor -HM-Sen-EP=Funk-Sensor fr elektrische Impulse -HM-Sen-LI-O=Funk-Helligkeitsensor fr Auenmontage -HM-Sen-MDIR-O=Funk-Bewegungsmelder auen +HM-Sen-EP=Funk-Sensor für elektrische Impulse +HM-Sen-LI-O=Funk-Helligkeitsensor für Außenmontage +HM-Sen-MDIR-O=Funk-Bewegungsmelder außen HM-Sen-MDIR-SM=Funk-Bewegungsmelder HM-Sen-MDIR-WM55=Funk-Bewegungsmelder mit Tastenpaar HM-Sen-RD-O=Regensensor -HM-Sen-Wa-Od=Kapazitiver Fllstandsmesser +HM-Sen-Wa-Od=Kapazitiver Füllstandsmesser HM-SwI-3-FM=Funk-Schalterschnittstelle 3-fach, Unterputzmontage HM-Sys-sRP-Pl=Funk-Zwischenstecker Repeater HM-TC-IT-WM-W-EU=Funk-Wandthermostat HM-WDC7000=Funk-Wetterstation WDC 7000 -HM-WDS10-TH-O=Funk-Temperatur-/ Feuchtesensor auen +HM-WDS10-TH-O=Funk-Temperatur-/ Feuchtesensor außen HM-WDS100-C6-O=Funk-Kombisensor (OC3) HM-WDS30-OT2-SM=Funk-Temperaturdifferenz-Sensor -HM-WDS30-T-O=Funk-Temperatursensor auen +HM-WDS30-T-O=Funk-Temperatursensor außen HM-WDS40-TH-I=Funk-Temperatursensor innen HM-WS550-US=Funk-Wetterstation USA -HM-WS550ST-IO=Funk-Temperatursensor auen +HM-WS550ST-IO=Funk-Temperatursensor außen HM-WS550STH-I=Funk-Temperatursensor innen -HM-WS550STH-O=Funk-Temperatur-/ Feuchtesensor auen -HMW-IO-12-FM=Wired RS485 I/O-Modul 12 Eingnge, Unterputzmontage -HMW-IO-12-Sw14-DR=Wired RS485 I/O-Modul 12 Eingnge, 14 Ausgnge, Hutschienenmontage -HMW-IO-12-Sw7-DR=Wired RS485 I/O-Modul 12 Eingnge, 7 Ausgnge, Hutschienenmontage -HMW-IO-4-FM=Wired RS485 I/O-Modul 4 Eingnge, Unterputzmontage +HM-WS550STH-O=Funk-Temperatur-/ Feuchtesensor außen +HMW-IO-12-FM=Wired RS485 I/O-Modul 12 Eingänge, Unterputzmontage +HMW-IO-12-Sw14-DR=Wired RS485 I/O-Modul 12 Eingänge, 14 Ausgänge, Hutschienenmontage +HMW-IO-12-Sw7-DR=Wired RS485 I/O-Modul 12 Eingänge, 7 Ausgänge, Hutschienenmontage +HMW-IO-4-FM=Wired RS485 I/O-Modul 4 Eingänge, Unterputzmontage HMW-LC-Bl1-DR=Wired RS485 Rollladenaktor 1-fach, Hutschienenmontage HMW-LC-Dim1L-DR=Wired RS485 Dimmaktor 1-fach, Phasenanschnitt, Hutschienenmontage HMW-LC-Sw2-DR=Wired RS485 Schaltaktor 2-fach, Hutschienenmontage HMW-RCV-50=Virtuelle Fernbedienung (drahtgebunden) HMW-Sec-TR-FM=Wired RS485 Transponderleser Unterputzmontage -HMW-Sen-SC-12-DR=Wired RS485 Schlieerkontakt, Hutschienenmontage -HMW-Sen-SC-12-FM=Wired RS485 Schlieerkontakt 12 Eingnge, Unterputzmontage +HMW-Sen-SC-12-DR=Wired RS485 Schließerkontakt, Hutschienenmontage +HMW-Sen-SC-12-FM=Wired RS485 Schließerkontakt 12 Eingänge, Unterputzmontage HMW-Sys-PS7-DR=Wired RS485-Netzteil 7 VA, Hutschienenmontage HMW-WSE-SM=Wired RS485 Lichtsensor Aufputzmontage HMW-WSTH-SM=Wired RS485 Temperatur-/ Feuchte- Sensor -HmIP-BBL=Homematic IP Jalousieaktor fr Markenschalter -HmIP-BROLL=Homematic IP Rollladenaktor fr Markenschalter +HmIP-ASIR=Homematic IP Alarmsirene +HmIP-ASIR-O=Homematic IP Alarmsirene - außen +HmIP-BBL=Homematic IP Jalousieaktor für Markenschalter +HmIP-BRC2=Homematic IP Wandtaster für Markenschalter - 2-fach +HmIP-BROLL=Homematic IP Rollladenaktor für Markenschalter +HmIP-BSL=Homematic IP Schaltaktor für Markenschalter - mit Signalleuchte +HmIP-CCU3=Homematic IP Zentrale CCU3 +HmIP-DBB=Homematic IP Türklingeltaster +HmIP-DLD=Homematic IP Türschlossantrieb +HmIP-DRBLI4=Homematic IP Jalousie- u. Rollladenaktor für Hutschienenmontage - 4-fach +HmIP-DRDI3=Homematic IP Dimmaktor für Hutschienenmontage - 3-fach +HmIP-DRS4=Homematic IP Schaltaktor für Hutschienenmontage - 4-fach +HmIP-DRSI1=Homematic IP Schaltaktor für Hutschienenmontage - 1-fach +HmIP-DRSI4=Homematic IP Schaltaktor für Hutschienenmontage - 4-fach +HmIP-DSD-PCB=Homematic IP Klingelsignalsensor +HmIP-FAL230-C10=Homematic IP Fußbodenheizungsaktor - 10-fach, 230 V +HmIP-FAL230-C6=Homematic IP Fußbodenheizungsaktor - 6-fach, 230 V +HmIP-FAL24-C10=Homematic IP Fußbodenheizungsaktor - 10-fach, 24 V +HmIP-FAL24-C6=Homematic IP Fußbodenheizungsaktor - 6-fach, 24 V +HmIP-FALMOT-C12=Homematic IP Fußbodenheizungsaktor - 12-fach, motorisch HmIP-FBL=Homematic IP Jalousieaktor, Unterputzmontage +HmIP-FCI1=Homematic IP Kontakt-Schnittstelle Unterputz - 1-fach +HmIP-FCI6=Homematic IP Kontakt-Schnittstelle Unterputz - 6-fach HmIP-FROLL=Homematic IP Rollladenaktor, Unterputzmontage +HmIP-FSI16=Homematic IP Schaltaktor mit Tastereingang (16 A) - Unterputz +HmIP-HAP=LAN ROUTER +HmIP-KRCK=Homematic IP Schlüsselbundfernbedienung - Zutritt +HmIP-MIO16-PCB=Homematic IP Multi IO Modulpatine - 4x4 +HmIP-MOD-HO=Homematic IP Modul für Hörmann-Antriebe HmIP-MOD-OC8=Homematic IP Schaltaktor mit OC-Ausgang -HmIP-PCBS=Homematic IP Funk-Schaltaktor 1-fach, Platine -HmIP-SAM=Homematic IP Erschtterungs- / Beschleunigungssensor -HmIP-SPI=Homematic IP Prsenzmelder, innen -HmIP-STHO=Homematic IP Temperatur- und Luftfeuchtigkeitssensor - auen -KRC4=Homematic IP Schlsselbundfernbedienung 4 Tasten -KRCA=Homematic IP Schlsselbundfernbedienung 4 Tasten Alarm +HmIP-MOD-RC8=Homematic IP Modulplatine Sender - 8-fach +HmIP-MOD-TM=Homematic IP Tormatic Modul +HmIP-MP3P=Homematic IP MP3 Kombisignalgeber +HmIP-PCBS=Homematic IP Schaltplatine +HmIP-PCBS-BAT=Homematic IP Schaltplatine für Batteriebetrieb +HmIP-PCBS2=Homematic IP Schaltplatine - 2-fach +HmIP-PMFS=Homematic IP Netzausfallüberwachung +HmIP-RCV-50=Virtuelle Fernbedienung +HmIP-SAM=Homematic IP Erschütterungs- / Beschleunigungssensor +HmIP-SCI=Homematic IP Kontakt-Schnittstelle +HmIP-SCTH230=Homematic IP CO2-Sensor, 230 V +HmIP-SFD=Homematic IP Feinstaubsensor +HmIP-SLO=Homematic IP Lichtsensor - außen +HmIP-SMI55=Homematic IP Bewegungsmelder für 55er Rahmen - innen +HmIP-SPDR=Homematic IP Durchgangssensor mit Richtungserkennung +HmIP-SPI=Homematic IP Präsenzmelder - innen +HmIP-SRD=Homematic IP Regensensor +HmIP-STE1-PCB=Homematic IP Temperatursensor mit externem Fühler - 1-fach +HmIP-STE2-PCB=Homematic IP Temperatursensor mit externen Fühlern - 2-fach +HmIP-STHD=Homematic IP Temperatur- und Luftfeuchtigkeitssensor mit Display - innen +HmIP-STHO=Homematic IP Temperatur- und Luftfeuchtigkeitssensor - außen +HmIP-STV=Homematic IP Neigungs- und Erschütterungssensor +HmIP-SWD=Homematic IP Wassersensor +HmIP-SWDM=Homematic IP Fenster- und Türkontakt mit Magnet +HmIP-SWDO=Homematic IP Fenster- und Türkontakt - optisch +HmIP-SWDO-I=Homematic IP Fenster- und Türkontakt - verdeckter Einbau +HmIP-SWDO-PL=Homematic IP Fenster- und Türkontakt - optisch, plus +HmIP-SWO-B=Homematic IP Wettersensor - basic +HmIP-SWO-PL=Homematic IP Wettersensor - plus +HmIP-SWO-PR=Homematic IP Wettersensor - pro +HmIP-USBSM=Homematic IP Schalt-Mess-Aktor für USB +HmIP-WGC=Homematic IP Garagentortaster +HmIP-WHS2=Homematic IP Schaltaktor für Heiz-Systeme - 2 Kanal +HmIP-WRC2=Homematic IP Wandtaster 2-fach +HmIP-WRCC2=Homematic IP Wandtaster - flach +HmIP-WRCD=Homematic IP Wandtaster mit Statusdisplay +HmIP-WRCR=Homematic IP Drehtaster +HmIP-WTH=Homematic IP Wandthermostat +HmIP-WTH-2=Homematic IP Wandthermostat mit Luftfeuchtigkeitssensor +HmIP-WTH-B=Homematic IP Wandthermostat - basic +HmIPW-BRC2=Homematic IP Wired Wandtaster für Markenschalter - 2-fach +HmIPW-DRAP=Homematic IP Wired Access Point +HmIPW-DRBL4=Homematic IP Wired Jalousie- u. Rollladenaktor - 4-fach +HmIPW-DRD3=Homematic IP Wired Dimmaktor - 3-fach +HmIPW-DRI16=Homematic IP Wired Eingangsmodul - 16-fach +HmIPW-DRI32=Homematic IP Wired Eingangsmodul - 32-fach +HmIPW-DRS4=Homematic IP Wired Schaltaktor - 4-fach +HmIPW-DRS8=Homematic IP Wired Schaltaktor - 8-fach +HmIPW-FAL230-C10=Homematic IP Wired Fußbodenheizungsaktor - 10-fach, 230 V +HmIPW-FAL230-C6=Homematic IP Wired Fußbodenheizungsaktor - 6-fach, 230 V +HmIPW-FAL24-C10=Homematic IP Wired Fußbodenheizungsaktor - 10-fach, 24 V +HmIPW-FAL24-C6=Homematic IP Wired Fußbodenheizungsaktor - 6-fach, 24 V +HmIPW-FIO6=Homematic IP Wired IO Modul Unterputz - 6-fach +HmIPW-SMI55=Homematic IP Wired Bewegungsmelder für 55er Rahmen - innen +HmIPW-SPI=Homematic IP Wired Präsenzmelder - innen +HmIPW-STH=Homematic IP Wired Temperatur- und Luftfeuchtigkeitssensor - innen +HmIPW-STHD=Homematic IP Wired Temperatur- und Luftfeuchtigkeitssensor mit Display - innen +HmIPW-WRC2=Homematic IP Wired Wandtaster - 2-fach +HmIPW-WRC6=Homematic IP Wired Wandtaster - 6-fach +HmIPW-WTH=Homematic IP Wired Wandthermostat mit Luftfeuchtigkeitssensor +KRC4=Homematic IP Schlüsselbundfernbedienung 4 Tasten +KRCA=Homematic IP Schlüsselbundfernbedienung 4 Tasten Alarm KS550=Funk-Kombisensor 550 MIOB=Homematic IP Multi I/O-Box OLIGO.smart.iq.HM=Funk-Dimmaktor @@ -209,24 +285,28 @@ PSM-IT=Homematic IP Zwischenstecker Schalten/Messen IT PSM-PE=Homematic IP Zwischenstecker Schalten/Messen Pin Earth PSM-UK=Homematic IP Zwischenstecker Schalten/Messen UK RC8=Homematic IP Fernbedienung, 8 Kanal +RPI-RF-MOD=CO-PROCESSOR SMI=Homematic IP Bewegungsmelder innen -SMO=Homematic IP Bewegungsmelder auen +SMO=Homematic IP Bewegungsmelder außen SRH=Homematic IP Fenster-/ Drehgriffkontakt -STH=Homematic IP Temperatur- und Luftfeuchtigkeitssensor, innen -STHD=Homematic IP Temperatur- und Luftfeuchtigkeitssensor mit Display, innen -SWD=Homematic IP Fenster- und Trkontakt optisch +STH=Homematic IP Temperatur- und Luftfeuchtigkeitssensor - innen SWSD=Homematic IP Rauchmelder -TRV=Homematic IP Heizkrperthermostat -TRV-UK=Homematic IP Heizkrperthermostat UK +TRV=Homematic IP Heizkörperthermostat +TRV-B=Homematic IP Heizkörperthermostat - basic +TRV-B-UK=Homematic IP Heizkörperthermostat - basic UK +TRV-C=Homematic IP Heizkörperthermostat - kompakt +TRV-E=Homematic IP Heizkörperthermostat - Evo +TRV-UK=Homematic IP Heizkörperthermostat UK +The END= +VIR-HUE-GTW=Philips-Hue Gateway VIR-OL-GTW=OSRAM-Lightify Gateway -WRC2=Homematic IP Wandtaster 2-fach WRC6=Homematic IP Wandtaster 6-fach WS888=Funk-Wetterstation -WTH=Homematic IP Wandthermostat +WT=Homematic IP Wandthermostat ZEL_STG_RM_DWT_10=Funk-Display-Wandtaster 2-fach, Aufputzmontage ZEL_STG_RM_FDK=Funk-Fenster-/ Drehgriffkontakt ZEL_STG_RM_FEP_230V=Funk-Rollladenaktor 1-fach, Unterputzmontage -ZEL_STG_RM_FFK=Funk-Tr-/ Fensterkontakt +ZEL_STG_RM_FFK=Funk-Tür-/ Fensterkontakt ZEL_STG_RM_FSA=Funk-Stellantrieb ZEL_STG_RM_FSS_UP3=Funk-Schalterschnittstelle 3-fach, Unterputzmontage ZEL_STG_RM_FST_UP4=Funk-Tasterschnittstelle 4-fach, Unterputzmontage @@ -239,9 +319,9 @@ atent=Funk-Handsender DORMA -ACCELERATION_TRANSCEIVER=Erschtterungs-/Beschleunigungssensor +ACCELERATION_TRANSCEIVER=Erschütterungs-/Beschleunigungssensor ACCELERATION_TRANSCEIVER|MOTION|FALSE=keine Bewegung/waagerecht -ACCELERATION_TRANSCEIVER|MOTION|TRUE=Bewegung erkannt/senkrecht +ACCELERATION_TRANSCEIVER|MOTION|TRUE=Bewegung erkannt/geneigt ACCELERATION_TRANSCEIVER|MSG_FOR_POS_A=Meldung in Position senkrecht ACCELERATION_TRANSCEIVER|MSG_FOR_POS_A|CLOSED=zu ACCELERATION_TRANSCEIVER|MSG_FOR_POS_A|NO_MSG=keine Meldung @@ -250,8 +330,10 @@ ACCELERATION_TRANSCEIVER|MSG_FOR_POS_B=Meldung in Position waagerecht ACCELERATION_TRANSCEIVER|MSG_FOR_POS_B|CLOSED=zu ACCELERATION_TRANSCEIVER|MSG_FOR_POS_B|NO_MSG=keine Meldung ACCELERATION_TRANSCEIVER|MSG_FOR_POS_B|OPEN=auf -ACOUSTIC_ALARM_SELECTION|DELAYED_EXTERNALLY_ARMED=Verzgert extern scharf -ACOUSTIC_ALARM_SELECTION|DELAYED_INTERNALLY_ARMED=Verzgert intern scharf +ACOUSTIC_ALARM_ACTIVE|FALSE=Akustisches Signal deaktiviert +ACOUSTIC_ALARM_ACTIVE|TRUE=Akustisches Signal aktiviert +ACOUSTIC_ALARM_SELECTION|DELAYED_EXTERNALLY_ARMED=Verzögert extern scharf +ACOUSTIC_ALARM_SELECTION|DELAYED_INTERNALLY_ARMED=Verzögert intern scharf ACOUSTIC_ALARM_SELECTION|DISABLE_ACOUSTIC_SIGNAL=Kein akustisches Signal ACOUSTIC_ALARM_SELECTION|DISARMED=Unscharf ACOUSTIC_ALARM_SELECTION|ERROR=Fehler @@ -268,10 +350,12 @@ ACOUSTIC_ALARM_SELECTION|FREQUENCY_RISING=Frequenz steigend ACOUSTIC_ALARM_SELECTION|FREQUENCY_RISING_AND_FALLING=Frequenz steigend/fallend ACOUSTIC_ALARM_SELECTION|INTERNALLY_ARMED=Intern scharf ACOUSTIC_ALARM_SELECTION|LOW_BATTERY=Batterie leer +ACOUSTIC_SIGNAL_VIRTUAL_RECEIVER=MP3-Player ACTUAL_HUMIDITY=Rel. Luftfeuchte ACTUAL_TEMPERATURE=Ist-Temperatur +AIR_PRESSURE=Luftdruck AKKU|LEVEL=Ladezustand -AKKU|STATUS|CHARGE=Ldt +AKKU|STATUS|CHARGE=Lädt AKKU|STATUS|DISCHARGE=Versorgung durch Akku AKKU|STATUS|STATE_UNKNOWN=Zustand unbekannt AKKU|STATUS|TRICKLE_CHARGE=Versorgung durch Akku @@ -283,81 +367,103 @@ ALARMACTUATOR|STATE|FALSE=Schaltzustand: Aus ALARMACTUATOR|STATE|TRUE=Schaltzustand: Ein ALARMTIME_MAX=Max. Alarmdauer ALARM_SWITCH_VIRTUAL_RECEIVER=Alarmsirene -ALL_LEDS=Alle Kanle einstellen +ALL_LEDS=Alle Kanäle einstellen ANALOG_INPUT=Analog +ANALOG_INPUT_TRANSMITTER|FILTER_SIZE=Anzahl der Messungen, die für die Mittelwertbildung der Eingangsspannung genutzt werden +ANALOG_INPUT_TRANSMITTER|VOLTAGE=Eingangsspannung ANALOG_OUTPUT=Analog ANALOG_OUTPUT_TRANSCEIVER=Analoger Ausgang ANALOG_OUTPUT_TRANSCEIVER|LEVEL=Ausgangspegel ARMING=Alarmsirene (Scharfschaltkanal) -ARMING|ACOUSTIC_ALLSENS_ARM=Akustisches Signal fr Scharfschaltung extern -ARMING|ACOUSTIC_ALLSENS_DELAY_ARM=Akustisches Signal fr Scharfschaltverzgerung extern -ARMING|ACOUSTIC_DISARM=Akustisches Signal fr Unscharfschaltung -ARMING|ACOUSTIC_EXTSENS_ARM=Akustisches Signal fr Scharfschaltung intern -ARMING|ACOUSTIC_EXTSENS_DELAY_ARM=Akustisches Signal fr Scharfschaltverzgerung intern -ARMING|ACOUSTIC_MULTI_DELAY_ARM=Akustische Signale fr verzgerte Scharfschaltungen mehrfach ausgeben -ARMING|OPTIC_ALLSENS_ARM=Optisches Signal fr Scharfschaltung extern -ARMING|OPTIC_ALLSENS_DELAY_ARM=Optisches Signal fr Scharfschaltverzgerung extern -ARMING|OPTIC_DISARM=Optisches Signal fr Unscharfschaltung -ARMING|OPTIC_EXTSENS_ARM=Optisches Signal fr Scharfschaltung intern -ARMING|OPTIC_EXTSENS_DELAY_ARM=Optisches Signal fr Scharfschaltverzgerung intern -ARMING|OPTIC_MULTI_DELAY_ARM=Optische Signale fr verzgerte Scharfschaltungen mehrfach ausgeben +ARMING|ACOUSTIC_ALLSENS_ARM=Akustisches Signal für Scharfschaltung extern +ARMING|ACOUSTIC_ALLSENS_DELAY_ARM=Akustisches Signal für Scharfschaltverzögerung extern +ARMING|ACOUSTIC_DISARM=Akustisches Signal für Unscharfschaltung +ARMING|ACOUSTIC_EXTSENS_ARM=Akustisches Signal für Scharfschaltung intern +ARMING|ACOUSTIC_EXTSENS_DELAY_ARM=Akustisches Signal für Scharfschaltverzögerung intern +ARMING|ACOUSTIC_MULTI_DELAY_ARM=Akustische Signale für verzögerte Scharfschaltungen mehrfach ausgeben +ARMING|OPTIC_ALLSENS_ARM=Optisches Signal für Scharfschaltung extern +ARMING|OPTIC_ALLSENS_DELAY_ARM=Optisches Signal für Scharfschaltverzögerung extern +ARMING|OPTIC_DISARM=Optisches Signal für Unscharfschaltung +ARMING|OPTIC_EXTSENS_ARM=Optisches Signal für Scharfschaltung intern +ARMING|OPTIC_EXTSENS_DELAY_ARM=Optisches Signal für Scharfschaltverzögerung intern +ARMING|OPTIC_MULTI_DELAY_ARM=Optische Signale für verzögerte Scharfschaltungen mehrfach ausgeben ARMSTATE|ALARM_BLOCKED=Alarm blockiert ARMSTATE|ALLSENS_ARMED=Alle Sensoren scharf, (extern scharf) ARMSTATE|DISARMED=Unscharf -ARMSTATE|EXTSENS_ARMED=Auensensoren scharf, (intern scharf) +ARMSTATE|EXTSENS_ARMED=Außensensoren scharf, (intern scharf) ARR_TIMEOUT=Bidirektionaler Kommunikationstimeout +ATC_ADAPTION_INTERVAL=Intervall für Temperaturkompensation der Sensoren +ATC_MODE=Temperaturkompensation der Sensoren ATC_OFF=Aus ATC_ON=Ein AUTO_MODE=Auto-Modus +AVERAGE_ILLUMINATION=Durchschnittliche Helligkeit BACKLIGHT_AT_CHARGE=Beleuchtung bei Betrieb in Ladeschale -BACKLIGHT_AT_KEYSTROKE=Beleuchtung bei Tastenbettigung -BACKLIGHT_AT_MOTION=Beleuchtung bei Bewegung/Erschtterung +BACKLIGHT_AT_KEYSTROKE=Beleuchtung bei Tastenbetätigung +BACKLIGHT_AT_MOTION=Beleuchtung bei Bewegung/Erschütterung BACKLIGHT_ON_TIME=Beleuchtungsdauer BATTERY_POWERED=Batteriebetrieb BATTERY_STATE=Batteriestatus BAT_DEFECT_LIMIT=Batterie-Defekt-Schwelle BLIND=Rollladenaktor -BLIND_TRANSMITTER|LEVEL=Behanghhe +BLIND_TRANSMITTER|ACTIVITY_STATE|DOWN=Jalousie fährt herunter +BLIND_TRANSMITTER|ACTIVITY_STATE|STABLE=Jalousie steht +BLIND_TRANSMITTER|ACTIVITY_STATE|UNKNOWN=Jalousie Aktivität unbekannt +BLIND_TRANSMITTER|ACTIVITY_STATE|UP=Jalousie fährt hoch +BLIND_TRANSMITTER|LEVEL=Behanghöhe BLIND_TRANSMITTER|LEVEL_2=Lamellenposition -BLIND_VIRTUAL_RECEIVER|LEVEL=Behanghhe +BLIND_TRANSMITTER|PROCESS|NOT_STABLE=Jalousie fährt +BLIND_TRANSMITTER|PROCESS|STABLE=Jalousie steht +BLIND_VIRTUAL_RECEIVER=Jalousieaktor +BLIND_VIRTUAL_RECEIVER|ACTIVITY_STATE|DOWN=Jalousie fährt herunter +BLIND_VIRTUAL_RECEIVER|ACTIVITY_STATE|STABLE=Jalousie steht +BLIND_VIRTUAL_RECEIVER|ACTIVITY_STATE|UNKNOWN=Jalousie Aktivität unbekannt +BLIND_VIRTUAL_RECEIVER|ACTIVITY_STATE|UP=Jalousie fährt hoch +BLIND_VIRTUAL_RECEIVER|LEVEL=Behanghöhe BLIND_VIRTUAL_RECEIVER|LEVEL_2=Lamellenposition +BLIND_VIRTUAL_RECEIVER|PROCESS|NOT_STABLE=Jalousie fährt +BLIND_VIRTUAL_RECEIVER|PROCESS|STABLE=Jalousie steht BLIND_VIRTUAL_RECEIVER|STOP=Anhalten BLIND|CHANGE_OVER_DELAY=Motorrichtungsumschaltzeit -BLIND|LEVEL=Behanghhe +BLIND|LEVEL=Behanghöhe BLIND|REFERENCE_RUNNING_TIME_BOTTOM_TOP=Fahrzeit von unten nach oben BLIND|REFERENCE_RUNNING_TIME_TOP_BOTTOM=Fahrzeit von oben nach unten BLIND|REFERENCE_RUN_COUNTER=Anzahl der Fahrten bis zur automatischen Kalibrierfahrt BLIND|STOP=Anhalten +BLOCKING_PERIOD_UNIT=Einheit des Zeitraums +BLOCKING_PERIOD_VALUE=Wert des Zeitraums BOOST_MODE=Boost-Funktion BOOST_MODE|FALSE=Boostmode AUS BOOST_MODE|TRUE=Boostmode EIN -BOOST_STATE=Boost-Dauer +BOOST_STATE=Boost-Status +BOOST_TIME=Boost-Dauer BRIGHTNESS=Helligkeit BRIGHTNESS_FILTER=Helligkeitsfilter +BRIGHTNESS_TRANSMITTER|FILTER_SIZE=Anzahl der zur Berechnung der Helligkeit verwendeten letzten Helligkeitswerte BURST_RX=Wake-On-Radio BUTTON_LOCK=Tastensperre BUTTON_RESPONSE_WITHOUT_BACKLIGHT=Sofortige Reaktion auf Tastendruck ohne vorherige Hintergrundbeleuchtung -CAPACITIVE_FILLING_LEVEL_SENSOR=Kapazitiver Fllstandsmesser -CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_DESIGN=Behlterform +CAPACITIVE_FILLING_LEVEL_SENSOR=Kapazitiver Füllstandsmesser +CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_DESIGN=Behälterform CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_DESIGN|HORIZONTAL_BARREL=liegender Zylinder CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_DESIGN|RECTANGLE=Quader CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_DESIGN|VERTICAL_BARREL=stehender Zylinder -CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_HIGH=Behlterhhe -CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_LENGTH=Behlterlnge -CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_WIDTH=Behlterweite -CAPACITIVE_FILLING_LEVEL_SENSOR|CONTROLTEXT_CALC_FILLINGLEVEL=Fllmenge wird berechnet. -CAPACITIVE_FILLING_LEVEL_SENSOR|CONTROLTEXT_FILLINGLEVEL=Fllmenge -CAPACITIVE_FILLING_LEVEL_SENSOR|FILLING_LEVEL=Aktueller Fllstand -CAPACITIVE_FILLING_LEVEL_SENSOR|FILL_LEVEL=Fllhhe bei 100 % -CAPACITIVE_FILLING_LEVEL_SENSOR|FILL_LEVEL_LOWER_THRESHOLD=untere Schaltschwelle in % fr zustzliche Benachrichtigung -CAPACITIVE_FILLING_LEVEL_SENSOR|FILL_LEVEL_UPPER_THRESHOLD=obere Schaltschwelle in % fr zustzliche Benachrichtigung -CAPACITIVE_FILLING_LEVEL_SENSOR|MEA_LENGTH=Lnge der Messleitung +CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_HIGH=Behälterhöhe +CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_LENGTH=Behälterlänge +CAPACITIVE_FILLING_LEVEL_SENSOR|CASE_WIDTH=Behälterweite +CAPACITIVE_FILLING_LEVEL_SENSOR|CONTROLTEXT_CALC_FILLINGLEVEL=Füllmenge wird berechnet. +CAPACITIVE_FILLING_LEVEL_SENSOR|CONTROLTEXT_FILLINGLEVEL=Füllmenge +CAPACITIVE_FILLING_LEVEL_SENSOR|FILLING_LEVEL=Aktueller Füllstand +CAPACITIVE_FILLING_LEVEL_SENSOR|FILL_LEVEL=Füllhöhe bei 100 % +CAPACITIVE_FILLING_LEVEL_SENSOR|FILL_LEVEL_LOWER_THRESHOLD=untere Schaltschwelle in % für zusätzliche Benachrichtigung +CAPACITIVE_FILLING_LEVEL_SENSOR|FILL_LEVEL_UPPER_THRESHOLD=obere Schaltschwelle in % für zusätzliche Benachrichtigung +CAPACITIVE_FILLING_LEVEL_SENSOR|MEA_LENGTH=Länge der Messleitung CAPACITIVE_FILLING_LEVEL_SENSOR|USE_CUSTOM=Benutzerdefinierte Kalibrierung -CAPACITIVE_FILLING_LEVEL_SENSOR|WATER_LOWER_THRESHOLD_CH=untere Schaltschwelle in % fr zustzliche Benachrichtigung -CAPACITIVE_FILLING_LEVEL_SENSOR|WATER_UPPER_THRESHOLD_CH=obere Schaltschwelle in % fr zustzliche Benachrichtigung +CAPACITIVE_FILLING_LEVEL_SENSOR|WATER_LOWER_THRESHOLD_CH=untere Schaltschwelle in % für zusätzliche Benachrichtigung +CAPACITIVE_FILLING_LEVEL_SENSOR|WATER_UPPER_THRESHOLD_CH=obere Schaltschwelle in % für zusätzliche Benachrichtigung CAPTURE_WITHIN_INTERVAL=Innerhalb des Sendeabstandes erkannte Bewegung senden: CENTRAL_KEY|DBL_PRESS_TIME=Doppelklick-Zeit (Tastensperre) -CENTRAL_KEY|LONG_PRESS_TIME=Mindestdauer fr langen Tastendruck +CENTRAL_KEY|LONG_PRESS_TIME=Mindestdauer für langen Tastendruck CHANGE_OVER|FALSE=Betriebsart Change Over nicht aktiv CHANGE_OVER|TRUE=Betriebsart Change Over aktiv CHARACTERISTIC_BASETYPE=Farbmischverhalten @@ -369,27 +475,35 @@ CHARACTERISTIC_HALF_CONSTANT=halbe/konstante Leistung CHARACTERISTIC_LEVELLIMIT=Pegelbegrenzung CHARACTERISTIC_LINEAR=linear CHARACTERISTIC_LINSQUARETYPE=Ausgangskennlinie -CHARACTERISTIC_LOW_IS_COLD=niedrig ist kaltwei -CHARACTERISTIC_LOW_IS_WARM=niedrig ist warmwei +CHARACTERISTIC_LOW_IS_COLD=niedrig ist kaltweiß +CHARACTERISTIC_LOW_IS_WARM=niedrig ist warmweiß CHARACTERISTIC_MAXIMUM=maximale Leistung CHARACTERISTIC_SQUARE=quadratisch CLIMATECONTROL_DEHUMIDIFIER_TRANSMITER=Sensor Luftfeuchte -CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|COOLING_DISABLE=Khlen im Khlmode +CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|COOLING_DISABLE=Kühlen im Kühlmode CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|FLOOR_HEATING_MODE=Art/Typ der Anlage CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|HEATING_DISABLE=Heizen im Heizmode CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|HEATING_MODE_SELECTION=Art des Raumes CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|HEATING_VALVE_TYPE=Ventiltyp -CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|HUMIDITY_LIMIT_DISABLE=Luftfeuchtigkeits-Schwellwert im Khlbetrieb +CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|HUMIDITY_LIMIT_DISABLE=Luftfeuchtigkeits-Schwellwert im Kühlbetrieb CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|HUMIDITY_LIMIT_VALUE=Luftfeuchtigkeitsschwelle CLIMATECONTROL_FLOOR_DIRECT_TRANSMITTER|MINIMAL_FLOOR_TEMPERATURE=Minimale Bodentemperatur CLIMATECONTROL_FLOOR_PUMP_TRANSCEIVER=Fussbodenheizung/Pumpensteuerung +CLIMATECONTROL_FLOOR_PUMP_TRANSCEIVER|EMERGENCY_OPERATION|FALSE=Verbindung zum RBG OK +CLIMATECONTROL_FLOOR_PUMP_TRANSCEIVER|EMERGENCY_OPERATION|TRUE=Verbindungsabbruch zum RBG CLIMATECONTROL_FLOOR_TRANSCEIVER=Fussbodenheizung +CLIMATECONTROL_FLOOR_TRANSCEIVER|EMERGENCY_OPERATION|FALSE=Verbindung zum RBG OK +CLIMATECONTROL_FLOOR_TRANSCEIVER|EMERGENCY_OPERATION|TRUE=Verbindungsabbruch zum RBG CLIMATECONTROL_FLOOR_TRANSMITTER=Raumthermostat +CLIMATECONTROL_FLOOR_TRANSMITTER|EMERGENCY_OPERATION|FALSE=Verbindung zum RBG OK +CLIMATECONTROL_FLOOR_TRANSMITTER|EMERGENCY_OPERATION|TRUE=Verbindungsabbruch zum RBG CLIMATECONTROL_HEAT_DEMAND_BOILER_TRANSMITTER=Kanal Heizbedarf CLIMATECONTROL_HEAT_DEMAND_PUMP_TRANSMITTER=Kanal Heizbedarf CLIMATECONTROL_HEAT_DEMAND_TRANSMITER=Kanal Heizbedarf -CLIMATECONTROL_RECEIVER=Heizungsthermostat (Empfnger Wandthermostat) -CLIMATECONTROL_REGULATOR=Heizkrperthermostat +CLIMATECONTROL_INPUT_RECEIVER=Kanal Heizen/Kühlen +CLIMATECONTROL_INPUT_TRANSMITTER=Kanal Eingangsstatus +CLIMATECONTROL_RECEIVER=Heizungsthermostat (Empfänger Wandthermostat) +CLIMATECONTROL_REGULATOR=Heizkörperthermostat CLIMATECONTROL_REGULATOR|ADJUSTING_COMMAND=Stellbefehl CLIMATECONTROL_REGULATOR|ADJUSTING_DATA=Stelldaten CLIMATECONTROL_REGULATOR|DECALCIFICATION_DAY=Entkalkungsfahrt @@ -415,69 +529,83 @@ CLIMATECONTROL_REGULATOR|MODE_TEMPERATUR_VALVE|CLOSE_VALVE=geschlossen CLIMATECONTROL_REGULATOR|MODE_TEMPERATUR_VALVE|OPEN_VALVE=offen CLIMATECONTROL_REGULATOR|PARTY_END_TIME=Party/Urlaub-Endzeit CLIMATECONTROL_REGULATOR|SETPOINT=Sollwert -CLIMATECONTROL_REGULATOR|SETPOINT|VENT_CLOSED=Ventil schlieen -CLIMATECONTROL_REGULATOR|SETPOINT|VENT_OPEN=Ventil ffnen -CLIMATECONTROL_REGULATOR|STATE|FALSE=Ventil schlieen -CLIMATECONTROL_REGULATOR|STATE|TRUE=Ventil ffnen +CLIMATECONTROL_REGULATOR|SETPOINT|VENT_CLOSED=Ventil schließen +CLIMATECONTROL_REGULATOR|SETPOINT|VENT_OPEN=Ventil öffnen +CLIMATECONTROL_REGULATOR|STATE|FALSE=Ventil schließen +CLIMATECONTROL_REGULATOR|STATE|TRUE=Ventil öffnen CLIMATECONTROL_REGULATOR|TEMPERATUR_COMFORT_VALUE=Comfort-Temperatur CLIMATECONTROL_REGULATOR|TEMPERATUR_LOWERING_VALUE=Eco-Temperatur CLIMATECONTROL_REGULATOR|TEMPERATUR_PARTY_VALUE=Party/Urlaub-Temperatur CLIMATECONTROL_REGULATOR|TEMPERATUR_SET_POINT=Solltemperatur CLIMATECONTROL_REGULATOR|TEMPERATUR_WINDOW_OPEN_VALUE=Fenster-Auf-Temperatur -CLIMATECONTROL_RT_RECEIVER=Heizungsthermostat (Empfnger) +CLIMATECONTROL_RT_RECEIVER=Heizungsthermostat (Empfänger) CLIMATECONTROL_RT_TRANSCEIVER=Heizungsthermostat (Sender) -CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|ADJUSTING_RANGE_TOO_LARGE=Stellbereich zu gro +CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|ADJUSTING_RANGE_TOO_LARGE=Stellbereich zu groß CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|ADJUSTING_RANGE_TOO_SMALL=Stellbereich zu klein -CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|COMMUNICATION_ERROR=Kommunikationsstrung +CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|COMMUNICATION_ERROR=Kommunikationsstörung CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|LOWBAT=Leere Batterie CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|NO_FAULT=kein Fehler CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|VALVE_ERROR_POSITION=Ventilfehlerposition CLIMATECONTROL_RT_TRANSCEIVER|FAULT_REPORTING|VALVE_TIGHT=Ventil fest -CLIMATECONTROL_VENT_DRIVE=Funk-Heizkrperthermostat +CLIMATECONTROL_VENT_DRIVE=Funk-Heizkörperthermostat CLIMATECONTROL_VENT_DRIVE|ERROR|ADJUSTING_RANGE_TO_SMALL=Stellbereich zu klein -CLIMATECONTROL_VENT_DRIVE|ERROR|LOWBAT=Strungsposition angefahren, Batterien nahezu entladen -CLIMATECONTROL_VENT_DRIVE|ERROR|VALVE_DRIVE_BLOCKED=Ventilantrieb schwergngig oder blockiert +CLIMATECONTROL_VENT_DRIVE|ERROR|LOWBAT=Störungsposition angefahren, Batterien nahezu entladen +CLIMATECONTROL_VENT_DRIVE|ERROR|VALVE_DRIVE_BLOCKED=Ventilantrieb schwergängig oder blockiert CLIMATECONTROL_VENT_DRIVE|ERROR|VALVE_DRIVE_LOOSE=Ventilantrieb nicht montiert oder Stellbereich zu gross -CLIMATECONTROL_VENT_DRIVE|VALVE_ERROR_POSITION=Ventilantrieb Strungsposition +CLIMATECONTROL_VENT_DRIVE|VALVE_ERROR_POSITION=Ventilantrieb Störungsposition CLIMATECONTROL_VENT_DRIVE|VALVE_OFFSET_VALUE=Ventilantrieb Offsetstellung CLIMATECONTROL_VENT_DRIVE|VALVE_STATE=Ventilantrieb Status +COLOR|LEVEL=Helligkeit +COMBINED_PARAMETER=Kanalaktion COMFORT_MODE=Comfort-Temperatur -COMPATIBILITY_MODE=Kompatibilittsmodus +COMPATIBILITY_MODE=Kompatibilitätsmodus CONDITION_CURRENT=Strom-Sensor CONDITION_FREQUENCY=Frequenz-Sensor CONDITION_POWER=Leistungs-Sensor CONDITION_VOLTAGE=Spannungs-Sensor -COND_TX_CYCLIC_ABOVE=Entscheidungswert zyklisch senden, wenn oberer Grenzwert berschritten -COND_TX_CYCLIC_BELOW=Entscheidungswert zyklisch senden, wenn unterer Grenzwert unterschritten -COND_TX_DECISION_ABOVE=Gesendeter Entscheidungswert, wenn oberer Grenzwert berschritten +COND_SWITCH_TRANSMITTER=Sender Entscheidungswert +COND_SWITCH_TRANSMITTER_BRIGHTNESS=Helligkeitssensor +COND_SWITCH_TRANSMITTER_HUMIDITY=Luftfreuchtesensor +COND_SWITCH_TRANSMITTER_RAIN_DROP=Regensensor +COND_SWITCH_TRANSMITTER_RAIN_QUANTITY=Sensor Regenmenge +COND_SWITCH_TRANSMITTER_TEMPERATURE=Temperatursensor +COND_SWITCH_TRANSMITTER_WIND_DIRECTION=Windrichtungssensor +COND_SWITCH_TRANSMITTER_WIND_SPEED=Windgeschwindigkeitssensor +COND_TX_CYCLIC_ABOVE=Entscheidungswert zyklisch senden +COND_TX_CYCLIC_BELOW=Entscheidungswert zyklisch senden +COND_TX_DECISION_ABOVE=Gesendeter Entscheidungswert, wenn oberer Grenzwert überschritten COND_TX_DECISION_BELOW=Gesendeter Entscheidungswert, wenn unterer Grenzwert unterschritten -COND_TX_FALLING=Bei Unterschreitung des unteren Grenzwerts Entscheidungswert senden, + -COND_TX_RISING=Bei berschreitung des oberen Grenzwerts Entscheidungswert senden, + +COND_TX_FALLING=Bei Unterschreitung des unteren Grenzwertes Entscheidungswert senden, + +COND_TX_RISING=Bei Überschreitung des oberen Grenzwertes Entscheidungswert senden, + COND_TX_THRESHOLD_HI=Oberer Grenzwert COND_TX_THRESHOLD_LO=Unterer Grenzwert -CONFIG_PENDING|FALSE=Konfigurationsdaten zur bertragung: Nein -CONFIG_PENDING|TRUE=Konfigurationsdaten zur bertragung: Ja -CONF_BUTTON_TIME=Fr die eingestellte Zeit nach Spannungszufuhr dient der lange Gertetastendruck zur Konfiguration, danach zur Bedienung. +CONFIG_PENDING|FALSE=Konfigurationsdaten zur Übertragung: Nein +CONFIG_PENDING|TRUE=Konfigurationsdaten zur Übertragung: Ja +CONF_BUTTON_TIME=Für die eingestellte Zeit nach Spannungszufuhr dient der lange Gerätetastendruck zur Konfiguration, danach zur Bedienung. CONF_BUTTON_TIME|PERMANENT=dauerhaft CONTROL_MODE|AUTO-MODE=Auto-Modus CONTROL_MODE|BOOST-MODE=Boost-Funktion CONTROL_MODE|MANU-MODE=Manu-Modus CONTROL_MODE|PARTY-MODE=Urlaubs-Modus +CURRENT=Strom CURRENTDETECTION_BEHAVIOR=Verhalten CURRENTDETECTION_BEHAVIOR|"CURRENTDETECTION_ACTIVE"=Wechselschaltung CURRENTDETECTION_BEHAVIOR|"CURRENTDETECTION_INACTIVE_VALUE_OUTPUT_1"=Ausgang 1 aktiv CURRENTDETECTION_BEHAVIOR|"CURRENTDETECTION_INACTIVE_VALUE_OUTPUT_2"=Ausgang 2 aktiv +CURRENT_ILLUMINATION=Ungefilterte, aktuelle Helligkeit +CURRENT_PASSAGE_DIRECTION|FALSE=Erkannter Durchgang: Nein +CURRENT_PASSAGE_DIRECTION|TRUE=Erkannter Durchgang: Ja CYCLIC_INFO_MSG=Zyklische Statusmeldung CYCLIC_INFO_MSG_DIS=Anzahl der auszulassenden Statusmeldungen -CYCLIC_INFO_MSG_DIS_UNCHANGED=Anzahl der auszulassenden, unvernderlichen Statusmeldungen +CYCLIC_INFO_MSG_DIS_UNCHANGED=Anzahl der auszulassenden, unveränderten Statusmeldungen CYCLIC_INFO_MSG_OVERDUE_THRESHOLD=Anzahl der verpassten Statusmeldungen, bis 'unreach' geflaggt wird -CYCLIC_INFO_MSG_PAUSE=Intervall fr zyklische Statusmeldung +CYCLIC_INFO_MSG_PAUSE=Intervall für zyklische Statusmeldung DATE_TIME_UNKNOWN|FALSE=Korrekte Zeit bekannt DATE_TIME_UNKNOWN|TRUE=Korrekte Zeit nicht bekannt DAYLIGHT_SAVINGS_TIME=Automatisches Umstellen von Sommer- auf Winterzeit -DDC|STATE=ffnen +DDC|STATE=Öffnen DECISION_VALUE=Entscheidungswert -DEVICE_LED_MODE=Gerte-LED +DEVICE_LED_MODE=Geräte-LED DEVICE_LED_MODE|OFF=aus DEVICE_LED_MODE|ON=ein DEV_RPT_CNT_MAX=max. Repeaterstufe @@ -502,32 +630,47 @@ DIGITAL_OUTPUT=Digital DIGITAL_OUTPUT|STATE|FALSE=Schaltzustand: aus DIGITAL_OUTPUT|STATE|TRUE=Schaltzustand: ein DIMMER=Dimmaktor +DIMMER_TRANSMITTER|ACTIVITY_STATE|DOWN=Rampe herunter +DIMMER_TRANSMITTER|ACTIVITY_STATE|STABLE=Pegel stabil +DIMMER_TRANSMITTER|ACTIVITY_STATE|UNKNOWN=Dimmer Aktivität unbekannt +DIMMER_TRANSMITTER|ACTIVITY_STATE|UP=Rampe hoch +DIMMER_TRANSMITTER|PROCESS|NOT_STABLE=Rampe aktiv +DIMMER_TRANSMITTER|PROCESS|STABLE=Pegel stabil DIMMER_VIRTUAL_RECEIVER=Dimmaktor +DIMMER_VIRTUAL_RECEIVER|ACTIVITY_STATE|DOWN=Rampe herunter +DIMMER_VIRTUAL_RECEIVER|ACTIVITY_STATE|STABLE=Pegel stabil +DIMMER_VIRTUAL_RECEIVER|ACTIVITY_STATE|UNKNOWN=Dimmer Aktivität unbekannt +DIMMER_VIRTUAL_RECEIVER|ACTIVITY_STATE|UP=Rampe hoch +DIMMER_VIRTUAL_RECEIVER|PROCESS|NOT_STABLE=Rampe aktiv +DIMMER_VIRTUAL_RECEIVER|PROCESS|STABLE=Pegel stabil DIMMER|CHARACTERISTIC=Ausgangskennlinie -DIMMER|ERROR_OVERHEAT=berhitzung -DIMMER|ERROR_OVERLOAD=berlastung +DIMMER|ERROR_OVERHEAT=Überhitzung +DIMMER|ERROR_OVERLOAD=Überlastung DIMMER|ERROR_REDUCED=Temperatur kritisch (Lastabsenkung) DIMMER|ERROR|LOAD_FAILURE=Lastfehler -DIMMER|FUSE_DELAY=Trgheit berstromerkennung +DIMMER|FUSE_DELAY=Trägheit Überstromerkennung DIMMER|LEVEL=Dimmwert DIMMER|LOAD_APPEAR_BEHAVIOUR=Aktion bei Last-Wiederkehr DIMMER|LOAD_ERROR_CALIB=Lastausfallkalibrierung -DIMMER|LOGIC_COMBINATION=Verknpfungsregel +DIMMER|LOGIC_COMBINATION=Verknüpfungsregel DIMMER|OLD_LEVEL=Letzter Dimmwert DIMMER|ON_TIME=Einschaltdauer -DIMMER|OVERTEMP_LEVEL=Abschaltschwelle bertemperatur +DIMMER|OVERTEMP_LEVEL=Abschaltschwelle Übertemperatur DIMMER|POWERUP_ACTION=Aktion bei Spannungszufuhr DIMMER|RAMP_STOP=Stop Dimmrampe DIMMER|RAMP_TIME=Dimmzeit -DIMMER|REDUCE_LEVEL=Reduzierpegel bertemperatur -DIMMER|REDUCE_TEMP_LEVEL=Reduzierschwelle bertemperatur -DIMMER|RELAY_OFFDELAY_TIME=Relaiszeit fr die Ausschaltverzgerung +DIMMER|REDUCE_LEVEL=Reduzierpegel Übertemperatur +DIMMER|REDUCE_TEMP_LEVEL=Reduzierschwelle Übertemperatur +DIMMER|RELAY_OFFDELAY_TIME=Relaiszeit für die Ausschaltverzögerung +DISABLE_ACOUSTIC_CHANNELSTATE=Summer deaktivieren +DISABLE_ACOUSTIC_SENDSTATE=Akustische Bestätigung des Tastendrucks deaktivieren DISPLAY_BACKLIGHT_MODE=Displayhinterleuchtungsmodus DISPLAY_BACKLIGHT_MODE|AUTO=automatisch DISPLAY_BACKLIGHT_MODE|OFF=aus DISPLAY_BACKLIGHT_MODE|ON=ein DISPLAY_BACKLIGHT_TIME=Displayhinterleuchtungszeit DISPLAY_BRIGHTNESS=Anzeigehelligkeit +DISPLAY_CONTRAST=Displaykontrast DISPLAY_ENERGYOPTIONS=Die Anzeige schaltet sich aus nach DISPLAY_INVERTING=Displayinvertierung DISPLAY|ALARM_COUNT=Anzahl Alarmmeldungen @@ -576,13 +719,13 @@ DISPLAY|BELL=Glockensymbol DISPLAY|BLIND=Rollladensymbol DISPLAY|BULB=Lampensymbol DISPLAY|CLOCK=Uhrsymbol -DISPLAY|DOOR=Trsymbol -DISPLAY|MESSAGE_SHOW_TIME=Anzeigedauer fr Zentralen-Nachrichten +DISPLAY|DOOR=Türsymbol +DISPLAY|MESSAGE_SHOW_TIME=Anzeigedauer für Zentralen-Nachrichten DISPLAY|MESSAGE_SHOW_TIME|PERMANENT=dauerhaft DISPLAY|PHONE=Telefonsymbol DISPLAY|SCENE=Szenensymbol DISPLAY|SERVICE_COUNT=Anzahl Servicemeldungen -DISPLAY|SUBMIT=Displaynachricht bertragen +DISPLAY|SUBMIT=Displaynachricht übertragen DISPLAY|SWITCH=Schaltersymbol DISPLAY|TEXT=Text DISPLAY|UNIT|CELSIUS=Einheit Celsius @@ -591,14 +734,25 @@ DISPLAY|UNIT|NONE=Keine Einheit DISPLAY|UNIT|PERCENT=Einheit Prozent DISPLAY|UNIT|WATT=Einheit Watt DISPLAY|WINDOW=Fenstersymbol +DOOR_COMMAND|CLOSE=Tor schließen +DOOR_COMMAND|NOP=keine Aktion +DOOR_COMMAND|OPEN=Tor öffnen +DOOR_COMMAND|PARTIAL_OPEN=Stellung Lüftungsposition +DOOR_COMMAND|STOP=Fahrt stoppen +DOOR_RECEIVER=Toröffner +DOOR_STATE|CLOSED=Stellung geschlossen +DOOR_STATE|OPEN=Stellung geöffnet +DOOR_STATE|POSITION_UNKNOWN=Stellung unbekannt +DOOR_STATE|VENTILATION_POSITION=Lüftungsposition DUAL_WHITE_BRIGHTNESS=Dual-White-Controller (Dimmer) DUAL_WHITE_COLOR=Dual-White-Controller (Farbe) DUAL_WHITE_COLOR|LEVEL=Farbwert DUAL_WHITE_COLOR|LEVEL_REAL=Farbwert Realkanal DUAL_WHITE_COLOR|OLD_LEVEL=Letzter Wert DUAL_WHITE_COLOR|RAMP_STOP=Stop Farbwechsel -DUAL_WHITE_COLOR|RAMP_TIME=Rampenzeit fr Farbwechsel +DUAL_WHITE_COLOR|RAMP_TIME=Rampenzeit für Farbwechsel DURATION_UNIT=Einheit Zeitdauer +DURATION_UNIT|10MS=Einheit Zeitdauer: 10 mS DURATION_UNIT|D=Einheit Zeitdauer: Tage DURATION_UNIT|H=Einheit Zeitdauer: Stunden DURATION_UNIT|M=Einheit Zeitdauer: Minuten @@ -610,75 +764,129 @@ DUTY_CYCLE|TRUE=Dutycycle erreicht EMERGENCY_OPERATION|FALSE=Verbindung zum RBG OK EMERGENCY_OPERATION|TRUE=Verbindungsabbruch zum RBG ENABLE_ROUTING=Routing aktiv -ENERGIE_METER_TRANSMITTER|AVERAGING=Mittelwertbildung ber -ENERGIE_METER_TRANSMITTER|CURRENT=Strom -ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER=Energie-Zhler Gert -ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER_OVERFLOW|FALSE=kein bertrag -ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER_OVERFLOW|TRUE=bertrag +ENERGIE_METER_TRANSMITTER|AVERAGING=Mittelwertbildung über +ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER=Energie-Zähler Gerät +ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER_OVERFLOW|FALSE=kein Übertrag +ENERGIE_METER_TRANSMITTER|ENERGY_COUNTER_OVERFLOW|TRUE=Übertrag ENERGIE_METER_TRANSMITTER|FREQUENCY=Frequenz ENERGIE_METER_TRANSMITTER|POWER=Leistung ENERGIE_METER_TRANSMITTER|TX_THRESHOLD_POWER=TX Differenz Leistung -ENERGIE_METER_TRANSMITTER|VOLTAGE=Spannung ERROR=Fehler +ERROR_BAD_RECHARGEABLE_BATTERY_HEALTH=Akkuzustand: nicht OK +ERROR_BAD_RECHARGEABLE_BATTERY_HEALTH|FALSE=Akkuzustand: OK +ERROR_BAD_RECHARGEABLE_BATTERY_HEALTH|TRUE=Akkuzustand: nicht OK +ERROR_BUS_CONFIG_MISMATCH=Die tatsächliche Bustopologie entspricht nicht der konfigurierten Bustopologie. +ERROR_BUS_CONFIG_MISMATCH|FALSE=Die tatsächliche Bustopologie entspricht der konfigurierten Bustopologie. +ERROR_BUS_CONFIG_MISMATCH|TRUE=Die tatsächliche Bustopologie entspricht nicht der konfigurierten Bustopologie. ERROR_CODE=Fehlercode -ERROR_OVERHEAT|FALSE=berhitzung: Nein -ERROR_OVERHEAT|TRUE=berhitzung: Ja -ERROR_OVERLOAD|FALSE=Stromberlastung: Nein -ERROR_OVERLOAD|TRUE=Stromberlastung: Ja +ERROR_COPROCESSOR=Der Kanal ist nicht erreichbar. Bitte prüfen Sie die Spannungsversorgung des Kanals oder deaktivieren Sie diesen in der WebUI. +ERROR_COPROCESSOR|FALSE=Fehler CoProcessor: Nein +ERROR_COPROCESSOR|TRUE=Fehler CoProcessor: Ja +ERROR_NON_FLAT_POSITIONING=Fehler Lageerkennung +ERROR_NON_FLAT_POSITIONING|FALSE=Winkel Lageerkennung überschritten: Nein +ERROR_NON_FLAT_POSITIONING|TRUE=Winkel Lageerkennung überschritten: Ja +ERROR_OVERHEAT=Überhitzung: Ja +ERROR_OVERHEAT|FALSE=Überhitzung: Nein +ERROR_OVERHEAT|TRUE=Überhitzung: Ja +ERROR_OVERLOAD=Stromüberlastung: Ja +ERROR_OVERLOAD|FALSE=Stromüberlastung: Nein +ERROR_OVERLOAD|TRUE=Stromüberlastung: Ja +ERROR_POWER_FAILURE=Spannungsversorgung fehlerhaft +ERROR_POWER_FAILURE|FALSE=Spannungsversorgung OK +ERROR_POWER_FAILURE|TRUE=Spannungsversorgung fehlerhaft +ERROR_POWER_SHORT_CIRCUIT_BUS_1=Ein Kurzschluss zwischen den Stromleitungen von Bus 1 wurde festgestellt. +ERROR_POWER_SHORT_CIRCUIT_BUS_1|FALSE=Kein Kurzschluss zwischen den Stromleitungen von Bus 1 festgestellt. +ERROR_POWER_SHORT_CIRCUIT_BUS_1|TRUE=Ein Kurzschluss zwischen den Stromleitungen von Bus 1 wurde festgestellt. +ERROR_POWER_SHORT_CIRCUIT_BUS_2=Ein Kurzschluss zwischen den Stromleitungen von Bus 2 wurde festgestellt. +ERROR_POWER_SHORT_CIRCUIT_BUS_2|FALSE=Kein Kurzschluss zwischen den Stromleitungen von Bus 2 festgestellt. +ERROR_POWER_SHORT_CIRCUIT_BUS_2|TRUE=Ein Kurzschluss zwischen den Stromleitungen von Bus 2 wurde festgestellt. ERROR_POWER|FALSE=Netzspannung fehlerhaft ERROR_POWER|TRUE=Netzspannung OK -ERROR_REDUCED|FALSE=volle Leistung mglich +ERROR_REDUCED|FALSE=volle Leistung möglich ERROR_REDUCED|TRUE=reduzierte Leistung -ERROR_SABOTAGE|FALSE=Sabotage ausgelst +ERROR_RESTART_NEEDED=Es ist ein Neutstart des Gerätes nötig. +ERROR_RESTART_NEEDED|FALSE=Neustart nötig: Nein +ERROR_RESTART_NEEDED|TRUE=Neustart nötig: Ja +ERROR_SABOTAGE|FALSE=Sabotage ausgelöst ERROR_SABOTAGE|TRUE=Sabotage OK -ERROR_UPDATE|FALSE=Fehler Gerte Update: Nein -ERROR_UPDATE|TRUE=Fehler Gerte Update: Ja +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_1=Ein Kurzschluss zwischen der 24V-Leitung und der Datenleitung A und/oder B von Bus 1 wurde festgestellt. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_1|FALSE=Kein Kurzschluss zwischen der 24V-Leitung und der Datenleitung A und/oder B von Bus 1 festgestellt. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_1|TRUE=Ein Kurzschluss zwischen der 24V-Leitung und der Datenleitung A und/oder B von Bus 1 wurde festgestellt. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_2=Ein Kurzschluss zwischen der 24V-Leitung und der Datenleitung A und/oder B von Bus 2 wurde festgestellt. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_2|FALSE=Kein Kurzschluss zwischen der 24V-Leitung und der Datenleitung A und/oder B von Bus 2 festgestellt. +ERROR_SHORT_CIRCUIT_DATA_LINE_BUS_2|TRUE=Ein Kurzschluss zwischen der 24V-Leitung und der Datenleitung A und/oder B von Bus 2 wurde festgestellt. +ERROR_UNDERVOLTAGE=Betriebsspannung nicht OK +ERROR_UNDERVOLTAGE|FALSE=Betriebsspannung OK +ERROR_UNDERVOLTAGE|TRUE=Betriebsspannung nicht OK +ERROR_UPDATE|FALSE=Fehler Geräte Update: Nein +ERROR_UPDATE|TRUE=Fehler Geräte Update: Ja +ERROR_WIND_COMMUNICATION|FALSE=Sensor Windrichtung: Kommunikation OK +ERROR_WIND_COMMUNICATION|TRUE=Sensor Windrichtung: Kommunikationsfehler +ERROR_WIND_NORTH|FALSE=Sensor Windrichtung: Nord kalibriert +ERROR_WIND_NORTH|TRUE=Sensor Windrichtung: Nord nicht kalibriert ERROR|NO_ERROR=Kein Fehler -EVENT_DELAY_UNIT=Einheit der Eventverzgerung -EVENT_DELAY_VALUE=Wert Eventverzgerung +EVENT_DELAYTIME=Meldeverzögerung +EVENT_DELAY_UNIT=Einheit der Eventverzögerung +EVENT_DELAY_VALUE=Wert Eventverzögerung EVENT_FILTER_NUMBER=Empfindlichkeit EVENT_FILTER_PERIOD=Filterzeitraum EVENT_RANDOMTIME_UNIT=Einheit des Zufallsanteils EVENT_RANDOMTIME_VALUE=Statusmeldungen Zufallsanteil -EXPECT_AES=AES-Verschlsselung +EXPECT_AES=AES-Verschlüsselung EXTERNAL_CLOCK|FALSE=Modus Energiespar-Temperatur nicht aktiv EXTERNAL_CLOCK|TRUE=Modus Energiespar-Temperatur aktiv +FREQUENCY_ALTERNATING_LOW_HIGH=Frequenz tief/hoch +FREQUENCY_ALTERNATING_LOW_MID_HIGH=Frequenz tief/mittel/hoch +FREQUENCY_FALLING=Frequenz fallend +FREQUENCY_HIGHON_LONGOFF=Frequenz hoch ein, lang aus +FREQUENCY_HIGHON_OFF=Frequenz hoch ein/aus FREQUENCY_INPUT=Analog +FREQUENCY_LOWON_LONGOFF_HIGHON_LONGOFF=Frequenz tief ein - lang aus, hoch ein - lang aus +FREQUENCY_LOWON_OFF_HIGHON_OFF=Frequenz tief ein/aus, hoch ein/aus +FREQUENCY_RISING=Frequenz steigend +FREQUENCY_RISING_AND_FALLING=Frequenz steigend/fallend FROST_PROTECTION_TEMPERATURE=Frostschutztemperatur FROST_PROTECTION|FALSE=Frostschutz nicht aktiv FROST_PROTECTION|TRUE=Frostschutz aktiv -FUSE_DELAY=Trgheit berstromerkennung +FUSE_DELAY=Trägheit Überstromerkennung GENERIC_INPUT_TRANSMITER=Eingangskanal -HEATING_CLIMATECONTROL_CL_RECEIVER=Empfnger Thermostat +HEATING_CLIMATECONTROL_CL_RECEIVER=Empfänger Thermostat HEATING_CLIMATECONTROL_CL_TRANSMITTER=Sender Thermostat -HEATING_CLIMATECONTROL_RECEIVER=Empfnger Thermostat +HEATING_CLIMATECONTROL_RECEIVER=Empfänger Thermostat HEATING_CLIMATECONTROL_SWITCH_TRANSMITTER=Sender Thermostat HEATING_CLIMATECONTROL_TRANSCEIVER=Sender Thermostat HEATING_CLIMATECONTROL_TRANSCEIVER|ACTIVE_PROFILE=Aktives Profil HEATING_CLIMATECONTROL_TRANSCEIVER|CONTROL_MODE=Modus Auto/Manu/Urlaub HEATING_CLIMATECONTROL_TRANSCEIVER|FROST_PROTECTION|FALSE=Frostschutz nicht aktiv HEATING_CLIMATECONTROL_TRANSCEIVER|FROST_PROTECTION|TRUE=Frostschutz aktiv -HEATING_CLIMATECONTROL_TRANSCEIVER|HUMIDITY=Luftfeuchtigkeit -HEATING_CLIMATECONTROL_TRANSCEIVER|LEVEL=Ventil-ffnungsgrad +HEATING_CLIMATECONTROL_TRANSCEIVER|HUMIDITY=Rel. Luftfeuchte +HEATING_CLIMATECONTROL_TRANSCEIVER|LEVEL=Ventil-Öffnungsgrad HEATING_CLIMATECONTROL_TRANSCEIVER|PARTY_MODE|FALSE=Urlaubsmodus nicht aktiv HEATING_CLIMATECONTROL_TRANSCEIVER|PARTY_MODE|TRUE=Urlaubsmodus aktiv -HEATING_CLIMATECONTROL_TRANSCEIVER|SET_POINT_MODE=Modus fr Solltemperatur +HEATING_CLIMATECONTROL_TRANSCEIVER|SET_POINT_MODE=Modus für Solltemperatur HEATING_CLIMATECONTROL_TRANSCEIVER|SET_POINT_TEMPERATURE=Solltemperatur -HEATING_CLIMATECONTROL_TRANSCEIVER|SWITCH_POINT_OCCURED|FALSE=Solltemperatur nicht gendert -HEATING_CLIMATECONTROL_TRANSCEIVER|SWITCH_POINT_OCCURED|TRUE=Solltemperatur gendert +HEATING_CLIMATECONTROL_TRANSCEIVER|SWITCH_POINT_OCCURED|FALSE=Solltemperatur nicht geändert +HEATING_CLIMATECONTROL_TRANSCEIVER|SWITCH_POINT_OCCURED|TRUE=Solltemperatur geändert HEATING_CLIMATECONTROL_TRANSCEIVER|VALVE_ADAPTION|FALSE=Adaptionsfahrt nicht aktiv HEATING_CLIMATECONTROL_TRANSCEIVER|VALVE_ADAPTION|TRUE=Adaptionsfahrt aktiv HEATING_CLIMATECONTROL_TRANSCEIVER|WINDOW_STATE|CLOSED=Fenster geschlossen -HEATING_CLIMATECONTROL_TRANSCEIVER|WINDOW_STATE|OPEN=Fenster geffnet -HEATING_KEY_RECEIVER=Empfnger Thermostat -HEATING_ROOM_TH_RECEIVER=Empfnger Thermostat +HEATING_CLIMATECONTROL_TRANSCEIVER|WINDOW_STATE|OPEN=Fenster geöffnet +HEATING_KEY_RECEIVER=Empfänger Thermostat +HEATING_ROOM_TH_RECEIVER=Empfänger Thermostat HEATING_ROOM_TH_TRANSCEIVER=Sender Thermostat -HEATING_SHUTTER_CONTACT_RECEIVER=Empfnger Thermostat +HEATING_SHUTTER_CONTACT_RECEIVER=Empfänger Thermostat +HIGHEST_ILLUMINATION=Maximale Helligkeit HUMIDITY=Rel. Luftfeuchte -HUMIDITY_ALARM|FALSE=Luftfeuchte nicht berschritten -HUMIDITY_ALARM|TRUE=Luftfeuchte berschritten +HUMIDITY_ALARM|FALSE=Luftfeuchte nicht überschritten +HUMIDITY_ALARM|TRUE=Luftfeuchte überschritten HUMIDITY_LIMITER|FALSE=Betriebsart Feuchtigkeitsbegrenzung nicht aktiv HUMIDITY_LIMITER|TRUE=Betriebsart Feuchtigkeitsbegrenzung aktiv +IDENTIFICATION_MODE_KEY_VISUAL|FALSE=Beleuchtung Systemtaste: AUS +IDENTIFICATION_MODE_KEY_VISUAL|TRUE=Beleuchtung Systemtaste: EIN +IDENTIFICATION_MODE_LCD_BACKLIGHT|FALSE=Beleuchtung Display: AUS +IDENTIFICATION_MODE_LCD_BACKLIGHT|TRUE=Beleuchtung Display: EIN +IDENTIFY_DURATION=Dauer der Beleuchtung +IDENTIFY_TARGET_LEVEL=Helligkeitswert der Beleuchtung ILLUMINATION=Helligkeit INHIBIT=Sperrung INHIBIT|FALSE=Sperrung inaktiv @@ -697,12 +905,12 @@ INPUT_OUTPUT|STATE|FALSE=Schaltzustand: aus INPUT_OUTPUT|STATE|TRUE=Schaltzustand: ein JALOUSIE=Jalousieaktor JALOUSIE|CHANGE_OVER_DELAY=Motorrichtungsumschaltzeit -JALOUSIE|LEVEL=Behanghhe -JALOUSIE|LEVEL_COMBINED=Gemeinsamer Wert: Behanghhe, Lamellenposition +JALOUSIE|LEVEL=Behanghöhe +JALOUSIE|LEVEL_COMBINED=Gemeinsamer Wert: Behanghöhe, Lamellenposition JALOUSIE|LEVEL_SLATS=Lamellenposition -JALOUSIE|LEVEL_SLATS|NO_MODIFICATION=Lamellenposition, keine nderung +JALOUSIE|LEVEL_SLATS|NO_MODIFICATION=Lamellenposition, keine Änderung JALOUSIE|LEVEL_SLATS|OLD_LEVEL=Lamellenposition, letzter Wert -JALOUSIE|LEVEL|NO_MODIFICATION=Keine nderung +JALOUSIE|LEVEL|NO_MODIFICATION=Keine Änderung JALOUSIE|LEVEL|OLD_LEVEL=Letzter Wert JALOUSIE|REFERENCE_RUNNING_TIME_BOTTOM_TOP=Fahrzeit von unten nach oben JALOUSIE|REFERENCE_RUNNING_TIME_SLATS=Lamellenverstellzeit @@ -713,14 +921,14 @@ KEY=Taster KEYMATIC=KeyMatic KEYMATIC|ANGLE_LOCKED=Drehwinkel Stellung verriegelt KEYMATIC|ANGLE_MAX=Drehwinkel Endanschlag verriegelt -KEYMATIC|ANGLE_OPEN=Drehwinkel Endanschlag Tr ffnen +KEYMATIC|ANGLE_OPEN=Drehwinkel Endanschlag Tür öffnen KEYMATIC|ERROR|CLUTCH_FAILURE=Einkuppeln fehlgeschlagen KEYMATIC|ERROR|MOTOR_ABORTED=Motorlauf abgebrochen -KEYMATIC|HOLD_PWM=Motorkraft Haltezeit Tr ffnen -KEYMATIC|HOLD_TIME=Haltezeit Tr ffnen +KEYMATIC|HOLD_PWM=Motorkraft Haltezeit Tür öffnen +KEYMATIC|HOLD_TIME=Haltezeit Tür öffnen KEYMATIC|LED_FLASH_LOCKED=LED blinkt wenn verriegelt KEYMATIC|LED_FLASH_UNLOCKED=LED blinkt wenn nicht verriegelt -KEYMATIC|OPEN=Tr ffnen +KEYMATIC|OPEN=Tür öffnen KEYMATIC|RELOCK_DELAY=Automatisch verriegeln nach Zeit KEYMATIC|RELOCK_DELAY|NOT_USED=Nicht automatisch verriegeln KEYMATIC|SETUP_DIR=Drehrichtung verriegeln @@ -733,10 +941,15 @@ KEYMATIC|STATE|FALSE=Schloss verriegelt KEYMATIC|STATE|TRUE=Schloss entriegelt KEYPRESS_SIGNAL=Tastenton KEY_TRANSCEIVER=Taster +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE=Kanalverhalten +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE|BINARY_BEHAVIOR=Kontakt +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE|INACTIVE=nicht aktiv +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE|KEY_BEHAVIOR=Taster +KEY_TRANSCEIVER|CHANNEL_OPERATION_MODE|SWITCH_BEHAVIOR=Schalter KEY_TRANSCEIVER|DBL_PRESS_TIME=Doppelklick-Zeit (Tastensperre) -KEY_TRANSCEIVER|LONG_PRESS_TIME=Mindestdauer fr langen Tastendruck +KEY_TRANSCEIVER|LONG_PRESS_TIME=Mindestdauer für langen Tastendruck KEY|CHANNEL_FUNCTION=Kanalfunktion -KEY|CHANNEL_FUNCTION|BINARY_BEHAVIOR=Tr/Fensterkontakt +KEY|CHANNEL_FUNCTION|BINARY_BEHAVIOR=Tür/Fensterkontakt KEY|CHANNEL_FUNCTION|BUTTON_BEHAVIOR=Taster (Toggle-Modus) KEY|CHANNEL_FUNCTION|INACTIVE=nicht aktiv KEY|CHANNEL_FUNCTION|SWITCH_BEHAVIOR=Schalter (2-Tasten-Modus) @@ -747,8 +960,8 @@ KEY|INPUT_TYPE|PUSHBUTTON=Taster KEY|INPUT_TYPE|SWITCH=Schalter KEY|LCD_LEVEL_INTERP=Text in der Anzeige KEY|LCD_SYMBOL=Symbol in der Anzeige -KEY|LONGPRESS_TIME=Mindestdauer fr langen Tastendruck -KEY|LONG_PRESS_TIME=Mindestdauer fr langen Tastendruck +KEY|LONGPRESS_TIME=Mindestdauer für langen Tastendruck +KEY|LONG_PRESS_TIME=Mindestdauer für langen Tastendruck KEY|MSG_FOR_POS_A=Meldung in Position geschlossen KEY|MSG_FOR_POS_A|CLOSED=zu KEY|MSG_FOR_POS_A|NO_MSG=keine Meldung @@ -764,48 +977,57 @@ KEY|TEXTLINE_2=Text Zeile LANGUAGE=Sprache LANGUAGE|ENGLISH=Englisch LANGUAGE|GERMAN=Deutsch -LED_DISABLE_CHANNELSTATE=Gerte-LED deaktivieren +LAST_PASSAGE_DIRECTION|FALSE=Letzter erkannter Durchgang: Nein +LAST_PASSAGE_DIRECTION|TRUE=Letzter erkannter Durchgang: Ja +LED_DISABLE_CHANNELSTATE=Geräte-LED deaktivieren +LED_DISABLE_LED_DISABLE_SENDSTATE=Visuelle Bestätigung des Tastendrucks deaktivieren LED_ONTIME=LED-Leuchtzeit (gn/rt) LED_SLEEP_MODE|OFF=Anzeige aus dem Ruhezustand holen LED_SLEEP_MODE|ON=Anzeige in Ruhezustand bringen -LED_STATUS|GREEN=Anzeige grn +LED_STATUS|GREEN=Anzeige grün LED_STATUS|OFF=Anzeige aus LED_STATUS|ORANGE=Anzeige orange LED_STATUS|RED=Anzeige rot -LEVEL=Dimmwert -LEVEL_REAL=Dimmwert Realkanal +LEVEL=Wert +LEVEL_REAL=Wert LIVE_MODE_RX=Live Mode -LOCAL_RESET_DISABLE=Reset per Gertetaste sperren -LOCAL_RESET_DISABLED=Reset per Gertetaste sperren +LOCAL_RESET_DISABLE=Reset per Gerätetaste sperren +LOCAL_RESET_DISABLED=Reset per Gerätetaste sperren LOGGING=Logging LOGGING_TIME=Zeit nach der Logging-Meldung verschickt wird LOGGING|FALSE=deaktiviert LOGGING|OFF=deaktiviert LOGGING|ON=aktiviert LOGGING|TRUE=aktiviert -LOGIC_AND=AND (niedriger Pegel hat Prioritt) +LOGIC_AND=AND (niedriger Pegel hat Priorität) LOGIC_ANDINVERS=AND_INVERS (AND mit vorheriger Invertierung des Kanal-Pegels) -LOGIC_COMBINATION=Verknpfungsregel +LOGIC_COMBINATION=Verknüpfungsregel LOGIC_INACTIVE=Kanal inaktiv -LOGIC_INVERSMINUS=INVERS_MINUS (wie MINUS, aber mit anschlieender Invertierung des Pegels) -LOGIC_INVERSMUL=INVERS_MULTI (wie MULTI, aber mit anschlieender Invertierung des Pegels) -LOGIC_INVERSPLUS=INVERS_PLUS (wie PLUS, aber mit anschlieender Invertierung des Pegels) +LOGIC_INVERSMINUS=INVERS_MINUS (wie MINUS, aber mit anschließender Invertierung des Pegels) +LOGIC_INVERSMUL=INVERS_MULTI (wie MULTI, aber mit anschließender Invertierung des Pegels) +LOGIC_INVERSPLUS=INVERS_PLUS (wie PLUS, aber mit anschließender Invertierung des Pegels) LOGIC_MINUS=MINUS (Pegel-Subtraktion, Minimum = 0%) LOGIC_MINUSINVERS=MINUS_INVERS (MINUS mit vorheriger Invertierung des Pegels) LOGIC_MUL=MULTI (Pegel-Multiplikation Maximum = 100%) LOGIC_MULINVERS=MULTI_INVERS (MULTI mit vorheriger Invertierung des Pegels) -LOGIC_NAND=NAND (wie AND, aber mit anschlieender Invertierung des Pegels) -LOGIC_NOR=NOR (wie OR, aber mit anschlieender Invertierung des Pegels) -LOGIC_OR=OR (hherer Pegel hat Prioritt) +LOGIC_NAND=NAND (wie AND, aber mit anschließender Invertierung des Pegels) +LOGIC_NOR=NOR (wie OR, aber mit anschließender Invertierung des Pegels) +LOGIC_OR=OR (höherer Pegel hat Priorität) LOGIC_ORINVERS=OR_INVERS (OR mit vorheriger Invertierung des Kanal-Pegels) LOGIC_PLUS=PLUS (Pegel-Addition, Maximum = 100%) LOGIC_PLUSINVERS=PLUS_INVERS (PLUS mit vorheriger Invertierung des Pegels) LOGIC_XOR=XOR (wie OR, aber wenn beide Pegel > 0, dann ist das Ergebnis 0) LOWBAT_SIGNAL=Low-Bat.-Signal LOWERING_MODE=Eco-Temperatur +LOWEST_ILLUMINATION=Minimale Helligkeit MAINS_POWERED=Netzbetrieb -MAINTENANCE|ERROR_OVERHEAT=berhitzung: Ja +MAINTENANCE|ERROR_OVERHEAT=Überhitzung: Ja +MAINTENANCE|ON_MIN_LEVEL=Ventilposition Umschaltwert +MAINTENANCE|PWM_AT_LOW_VALVE_POSITION=Automatische Umschaltung von Stetig auf PWM (bei kleinen Ventilpositionen) MANU_MODE=Manu-Modus +MASS_CONCENTRATION_PM_1=Massenkonzentration PM1.0 +MASS_CONCENTRATION_PM_10=Massenkonzentration PM10 +MASS_CONCENTRATION_PM_2_5=Massenkonzentration PM2.5 MIN_MAX_VALUE_NOT_RELEVANT_FOR_MANU_MODE=Ignoriere min./max. Temperatur im Manu-Mode MIOB_DIN_CONFIG=Modus digitaler Eingang MODUS_BUTTON_LOCK=Modus Bediensperre @@ -818,8 +1040,8 @@ MOD_EM8BIT_TRANSMITTER|DATA_INPUT_PROPERTIE_IN4=Eingang 4 MOD_EM8BIT_TRANSMITTER|DATA_INPUT_PROPERTIE_IN5=Eingang 5 MOD_EM8BIT_TRANSMITTER|DATA_INPUT_PROPERTIE_IN6=Eingang 6 MOD_EM8BIT_TRANSMITTER|DATA_INPUT_PROPERTIE_IN7=Eingang 7 -MOD_EM8BIT_TRANSMITTER|DATA_STABILITY_FILTER_TIME=Datenstabilittsfilterzeit vor der Sendung -MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION=Datenbertragungsbedingung +MOD_EM8BIT_TRANSMITTER|DATA_STABILITY_FILTER_TIME=Datenstabilitätsfilterzeit vor der Sendung +MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION=Datenübertragungsbedingung MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|LEVEL_CHANGE_DATA[HIGH_TO_LOW]=Modus 1 MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|LEVEL_CHANGE_DATA[LOW_TO_HIGH]=Modus 2 MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|LEVEL_CHANGE_DATA[LOW_TO_HIGH_AND_HIGH_TO_LOW]=Modus 3 @@ -828,8 +1050,10 @@ MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|NEW_DATA_SEND_IMMEDIATELY_DEF MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|NEW_DATA_STABLE_FOR_TIME_DEFAULT_DISABLE=Modus 6 MOD_EM8BIT_TRANSMITTER|DATA_TRANSMISSION_CONDITION|NEW_DATA_STABLE_FOR_TIME_DEFAULT_ENABLE=Modus 4 MOD_EM8BIT_TRANSMITTER|STATE=Wert des Dateneingangs +MOISTURE_DETECTED|FALSE=Feuchtigkeit erkannt: Nein +MOISTURE_DETECTED|TRUE=Feuchtigkeit erkannt: Ja MOTIONDETECTOR_TRANSCEIVER=Bewegungsmelder -MOTION_ACTIVE_TIME=Zeit, nach der erkannte Bewegung zurckgesetzt wird +MOTION_ACTIVE_TIME=Zeit, nach der erkannte Bewegung zurückgesetzt wird MOTION_DETECTION_ACTIVE|FALSE=Bewegungsmelder nicht aktiv MOTION_DETECTION_ACTIVE|TRUE=Bewegungsmelder aktiv MOTION_DETECTOR=Bewegungsmelder @@ -838,49 +1062,73 @@ MOTION_DETECTOR|ERROR|SABOTAGE=Sabotage MOTION_DETECTOR|MIN_INTERVAL=Mindestsendeabstand MOTION|FALSE=keine Bewegung MOTION|TRUE=Bewegung erkannt +MULTICAST_ROUTER_MODULE_ENABLED=MultiCast-Routing +MULTI_MODE_INPUT_TRANSMITTER=Eingangsmodul NOT_USED=Nicht benutzt +NUMBER_CONCENTRATION_PM_1=Mengenkonzentration PM1.0 +NUMBER_CONCENTRATION_PM_10=Mengenkonzentration PM10 +NUMBER_CONCENTRATION_PM_2_5=Mengenkonzentration PM2.5 OLD_LEVEL=Letzter Dimmwert ON_TIME=Einschaltdauer -OPERATING_VOLTAGE=Betriebspannung in V: -OPTICAL_ALARM_SELECTION|BLINKING_ALTERNATELY_REPEATING=Abwechselndes, langsames Blinken -OPTICAL_ALARM_SELECTION|BLINKING_BOTH_REPEATING=Gleichzeitigs langsames Blinken -OPTICAL_ALARM_SELECTION|CONFIRMATION_SIGNAL_0=Besttigungssignal 0 - lang lang -OPTICAL_ALARM_SELECTION|CONFIRMATION_SIGNAL_1=Besttigungssignal 1 - lang kurz -OPTICAL_ALARM_SELECTION|CONFIRMATION_SIGNAL_2=Besttigungssignal 2 - lang kurz kurz +OPERATING_VOLTAGE=Betriebsspannung in V +OPERATING_VOLTAGE_STATUS=Betriebsspannung in V +OPTICAL_ALARM_ACTIVE|FALSE=Optisches Signal deaktiviert +OPTICAL_ALARM_ACTIVE|TRUE=Optisches Signal aktiviert +OPTICAL_ALARM_SELECTION|BLINKING_ALTERNATELY_REPEATING=Abwechselndes langsames Blinken +OPTICAL_ALARM_SELECTION|BLINKING_BOTH_REPEATING=Gleichzeitiges langsames Blinken +OPTICAL_ALARM_SELECTION|CONFIRMATION_SIGNAL_0=Bestätigungssignal 0 - lang lang +OPTICAL_ALARM_SELECTION|CONFIRMATION_SIGNAL_1=Bestätigungssignal 1 - lang kurz +OPTICAL_ALARM_SELECTION|CONFIRMATION_SIGNAL_2=Bestätigungssignal 2 - lang kurz kurz OPTICAL_ALARM_SELECTION|DISABLE_OPTICAL_SIGNAL=Kein optisches Signal OPTICAL_ALARM_SELECTION|DOUBLE_FLASHING_REPEATING=Gleichzeitiges schnelles Blinken OPTICAL_ALARM_SELECTION|FLASHING_BOTH_REPEATING=Gleichzeitiges kurzes Blinken -OVERTEMP_LEVEL=Abschaltschwelle bertemperatur +OVERTEMP_LEVEL=Abschaltschwelle Übertemperatur PARAM_SELECT=Sensorauswahl Wettertelegramm -PARAM_SELECT|INACTIVE=Keine bertragung +PARAM_SELECT|INACTIVE=Keine Übertragung PARAM_SELECT|T1=Temperatur Sensor 1 -PARAM_SELECT|T1-T2=Differenz-Temperatursensor 1 - Sensor 2 +PARAM_SELECT|T1-T2=Differenz-Temperatur Sensor 1 - Sensor 2 PARAM_SELECT|T2=Temperatur Sensor 2 -PARAM_SELECT|T2-T1=Differenz-Temperatursensor 2 - Sensor 1 +PARAM_SELECT|T2-T1=Differenz-Temperatur Sensor 2 - Sensor 1 PARTY_SET_POINT_TEMPERATURE=Party/Urlaub-Temperatur +PARTY_START_DAY=Urlaub-Start-Tag +PARTY_START_MONTH=Urlaub-Start-Monat +PARTY_START_TIME=Urlaub-Start-Uhrzeit +PARTY_START_YEAR=Urlaub-Start-Jahr +PARTY_STOP_DAY=Urlaub-Ende-Tag +PARTY_STOP_MONTH=Urlaub-Ende-Monat +PARTY_STOP_TIME=Urlaub-Ende-Uhrzeit +PARTY_STOP_YEAR=Urlaub-Ende-Jahr +PARTY_TEMPERATURE=Urlaub-Temperatur PARTY_TIME_END=Party/Urlaub-Endzeit PARTY_TIME_START=Party/Urlaub-Startzeit +PASSAGE_COUNTER_OVERFLOW|FALSE=Durchgangszähler Überlauf: Nein +PASSAGE_COUNTER_OVERFLOW|TRUE=Durchgangszähler Überlauf: Ja +PASSAGE_COUNTER_VALUE=Anzahl der Durchgänge +PASSAGE_DETECTOR_COUNTER_TRANSMITTER=Durchgangszähler +PASSAGE_DETECTOR_COUNTER_TRANSMITTER|CHANNEL_OPERATION_MODE=Betriebsart +PASSAGE_DETECTOR_DIRECTION_TRANSMITTER=Richtungserkennung PEER_NEEDS_BURST=Burstsignal erforderlich PIR_OPERATION_MODE=Normal / Eco Modus -POSITION_SAVE_TIME=Position bernahmezeit -POWERMETER_IEC1|ENERGY_COUNTER=Energie-Zhler Gert -POWERMETER_IEC1|GAS_ENERGY_COUNTER=Energie Gas-Zhler Gert +PIR_SENSITIVITY=Sensorempfindlichkeit +POSITION_SAVE_TIME=Position Übernahmezeit +POWERMETER_IEC1|ENERGY_COUNTER=Energie-Zähler Gerät +POWERMETER_IEC1|GAS_ENERGY_COUNTER=Energie Gas-Zähler Gerät POWERMETER_IEC1|GAS_POWER=Gas Verbrauch -POWERMETER_IEC1|IEC_ENERGY_COUNTER=IEC Energie-Zhler Gert +POWERMETER_IEC1|IEC_ENERGY_COUNTER=IEC Energie-Zähler Gerät POWERMETER_IEC1|IEC_POWER=IEC Leistung POWERMETER_IEC1|POWER=Leistung -POWERMETER_IEC2|IEC_ENERGY_COUNTER=IEC Energie-Zhler Gert +POWERMETER_IEC2|IEC_ENERGY_COUNTER=IEC Energie-Zähler Gerät POWERMETER_IEC2|IEC_POWER=IEC Leistung -POWERMETER_IGL|ENERGY_COUNTER=Energie-Zhler Gert -POWERMETER_IGL|GAS_ENERGY_COUNTER=Energie Gas-Zhler Gert +POWERMETER_IGL|ENERGY_COUNTER=Energie-Zähler Gerät +POWERMETER_IGL|GAS_ENERGY_COUNTER=Energie Gas-Zähler Gerät POWERMETER_IGL|GAS_POWER=Gas Verbrauch POWERMETER_IGL|POWER=Leistung -POWERMETER|AVERAGING=Mittelwertbildung ber +POWERMETER|AVERAGING=Mittelwertbildung über POWERMETER|CURRENT=Strom -POWERMETER|ENERGY_COUNTER=Energie-Zhler Gert +POWERMETER|ENERGY_COUNTER=Energie-Zähler Gerät POWERMETER|FREQUENCY=Frequenz POWERMETER|POWER=Leistung -POWERMETER|TX_MINDELAY=Statusmeldung Mindestverzgerung +POWERMETER|TX_MINDELAY=Statusmeldung Mindestverzögerung POWERMETER|TX_THRESHOLD_CURRENT=TX Differenz Strom POWERMETER|TX_THRESHOLD_FREQUENCY=TX Differenz Frequenz POWERMETER|TX_THRESHOLD_POWER=TX Differenz Leistung @@ -889,21 +1137,30 @@ POWERMETER|VOLTAGE=Spannung POWERUP_ACTION=Aktion bei Spannungszufuhr POWERUP_JUMPTARGET=Aktion bei Spannungszufuhr POWERUP_OFF=keine +POWERUP_OFFDELAY_VALUE=Wert der Ausschaltverzögerung +POWERUP_OFFTIME_UNIT=Einheit der Ausschaltdauer POWERUP_ON=kurzen Tastendruck simulieren -POWERUP_ONDELAY_UNIT=Einheit der Einschaltverzgerung -POWERUP_ONDELAY_VALUE=Wert Einschaltverzgerung +POWERUP_ONDELAY_UNIT=Einheit der Einschaltverzögerung +POWERUP_ONDELAY_VALUE=Wert Einschaltverzögerung POWERUP_ONTIME_UNIT=Einheit der Einschaltdauer POWERUP_ONTIME_VALUE=Wert Einschaltdauer +POWER_MAINS_FAILURE|FALSE=Stromausfall: Nein +POWER_MAINS_FAILURE|TRUE=Stromausfall: Ja POWER_SUPPLY=Spannungsversorgung +PRESENCEDETECTOR_TRANSCEIVER=Präsenzmelder PRESENCEDETECTOR_TRANSCEIVER|MIN_INTERVAL=Mindestsendeabstand +PRESENCE_DETECTION_ACTIVE|FALSE=Präsenzdetector nicht aktiv +PRESENCE_DETECTION_ACTIVE|TRUE=Präsenzdetector aktiv +PRESENCE_DETECTION_STATE|FALSE=Keine Präsenz erkannt +PRESENCE_DETECTION_STATE|TRUE=Präsenz erkannt PRESS_LONG=Tastendruck lang PRESS_LONG|TRUE=Tastendruck lang PRESS_SHORT=Tastendruck kurz PRESS_SHORT|TRUE=Tastendruck kurz -PROCESS|NOT_STABLE=ZeitProgram: Aktiv -PROCESS|STABLE=Zeitprogram: Nicht aktiv +PROCESS|NOT_STABLE=Gerät aktiv +PROCESS|STABLE=Gerät nicht aktiv PULSE_SENSOR=Puls-Sensor -PULSE_SENSOR|SEQUENCE_OK=bettigt +PULSE_SENSOR|SEQUENCE_OK=betätigt PULSE_SENSOR|SEQUENCE_PULSE_1=Puls 1 in s PULSE_SENSOR|SEQUENCE_PULSE_1|NOT_USED=nicht benutzt PULSE_SENSOR|SEQUENCE_PULSE_2=Pause 1 in s @@ -915,21 +1172,44 @@ PULSE_SENSOR|SEQUENCE_PULSE_4|NOT_USED=nicht benutzt PULSE_SENSOR|SEQUENCE_PULSE_5=Puls 3 in s PULSE_SENSOR|SEQUENCE_PULSE_5|NOT_USED=nicht benutzt PULSE_SENSOR|SEQUENCE_TOLERANCE=Toleranz in s +RADIATOR_THERMOSTAT=Heizkörperthermostat RAINDETECTOR=Regensensor -RAINDETECTOR|COND_TX_THRESHOLD_HI=Erkennungsschwelle fr Trockenheit -RAINDETECTOR|COND_TX_THRESHOLD_LO=Erkennungsschwelle fr Regen -RAINDETECTOR|EVENT_FILTERTIME=Filterzeit fr Regenerkennung -RAINDETECTOR|EVENT_RELEASE_FILTER_TIME=Filterzeit fr Trockenheitserkennung -RAINDETECTOR|STATE_HIGH_HOLD_TIME=Abstand zur nchsten Messung bei erkanntem Regen +RAINDETECTOR|COND_TX_THRESHOLD_HI=Erkennungsschwelle für Trockenheit +RAINDETECTOR|COND_TX_THRESHOLD_LO=Erkennungsschwelle für Regen +RAINDETECTOR|EVENT_FILTERTIME=Filterzeit für Regenerkennung +RAINDETECTOR|EVENT_RELEASE_FILTER_TIME=Filterzeit für Trockenheitserkennung +RAINDETECTOR|STATE_HIGH_HOLD_TIME=Abstand zur nächsten Messung bei erkanntem Regen RAINDETECTOR|STATE|DRY=Trockenheit RAINDETECTOR|STATE|RAIN=Regen +RAINING=Regen +RAINING|FALSE=Aktuell kein Regen +RAINING|TRUE=Aktuell Regen +RAIN_COUNTER=Regenmenge +RAIN_COUNTER_OVERFLOW|FALSE=Regenzähler Überlauf: Nein +RAIN_COUNTER_OVERFLOW|TRUE=Regenzähler Überlauf: Ja +RAIN_DETECTION_TRANSMITTER=Regensensor RAMP_STOP=Stop Dimmrampe RAMP_TIME=Dimmzeit -REDUCE_LEVEL=Reduzierpegel bertemperatur -REDUCE_TEMP_LEVEL=Reduzierschwelle bertemperatur -REMOTECONTROL_RECEIVER=Heizungsthermostat (Empfnger Fernbedienung) -REPEATED_LONG_PRESS_TIMEOUT_UNIT=Einheit fr den Timeout -REPEATED_LONG_PRESS_TIMEOUT_VALUE=Wert fr den Timeout +RAMP_TIME_UNIT=Einheit Rampenzeit +RAMP_TIME_UNIT|10MS=Einheit Rampenzeit: 10 mS +RAMP_TIME_UNIT|D=Einheit Rampenzeit: Tage +RAMP_TIME_UNIT|H=Einheit Rampenzeit: Stunden +RAMP_TIME_UNIT|M=Einheit Rampenzeit: Minuten +RAMP_TIME_UNIT|S=Einheit Rampenzeit: Sekunden +RAMP_TIME_VALUE=Wert Rampenzeit +REDUCE_LEVEL=Reduzierpegel Übertemperatur +REDUCE_TEMP_LEVEL=Reduzierschwelle Übertemperatur +REFERENCE_RUNNING_TIME_BOTTOM_TOP_UNIT=Einheit der Fahrzeit +REFERENCE_RUNNING_TIME_BOTTOM_TOP_VALUE=Wert der Fahrzeit +REFERENCE_RUNNING_TIME_SLATS_UNIT=Einheit der Lamellenverstellzeit +REFERENCE_RUNNING_TIME_SLATS_VALUE=Wert der Lamellenverstellzeit +REFERENCE_RUNNING_TIME_TOP_BOTTOM_UNIT=Einheit der Fahrzeit +REFERENCE_RUNNING_TIME_TOP_BOTTOM_VALUE=Wert der Fahrzeit +REMOTECONTROL_RECEIVER=Heizungsthermostat (Empfänger Fernbedienung) +REPEATED_LONG_PRESS_TIMEOUT_UNIT=Einheit für den Timeout +REPEATED_LONG_PRESS_TIMEOUT_VALUE=Wert für den Timeout +RESET_MOTION=Reset Status +RESET_PRESENCE=Reset Status RESTART_BUTTONPRESS=kurzen Tastendruck simulieren RESTART_BUTTONPRESS_IF_WAS_ON=kurzen Tastendruck simulieren, falls zuvor eingeschaltet war RESTART_LAST=alten Zustand herstellen @@ -942,23 +1222,23 @@ RGBW_AUTOMATIC|PROGRAM=Programmnummer RGBW_COLOR=RGBW-Controller Farbe RGBW_COLOR|COLOR=Farbwert RGBW_COLOR|WHITE_ADJUSTMENT_VALUE_BLUE=Weissabgleich Blau -RGBW_COLOR|WHITE_ADJUSTMENT_VALUE_GREEN=Weissabgleich Grn +RGBW_COLOR|WHITE_ADJUSTMENT_VALUE_GREEN=Weissabgleich Grün RGBW_COLOR|WHITE_ADJUSTMENT_VALUE_RED=Weissableich Rot +ROTARY_CONTROL_TRANSCEIVER=Drehtaster ROTARY_HANDLE_SENSOR=Fenster-Drehgriffkontakt ROTARY_HANDLE_SENSOR|ERROR|SABOTAGE=Sabotage -ROTARY_HANDLE_SENSOR|EVENT_DELAYTIME=Meldeverzgerung ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A=Meldung in Position unten -ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A|CLOSED=zu +ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A|CLOSED=geschlossen ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A|NO_MSG=keine Meldung ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A|OPEN=offen ROTARY_HANDLE_SENSOR|MSG_FOR_POS_A|TILTED=gekippt ROTARY_HANDLE_SENSOR|MSG_FOR_POS_B=Meldung in Position quer -ROTARY_HANDLE_SENSOR|MSG_FOR_POS_B|CLOSED=zu +ROTARY_HANDLE_SENSOR|MSG_FOR_POS_B|CLOSED=geschlossen ROTARY_HANDLE_SENSOR|MSG_FOR_POS_B|NO_MSG=keine Meldung ROTARY_HANDLE_SENSOR|MSG_FOR_POS_B|OPEN=offen ROTARY_HANDLE_SENSOR|MSG_FOR_POS_B|TILTED=gekippt ROTARY_HANDLE_SENSOR|MSG_FOR_POS_C=Meldung in Position oben -ROTARY_HANDLE_SENSOR|MSG_FOR_POS_C|CLOSED=zu +ROTARY_HANDLE_SENSOR|MSG_FOR_POS_C|CLOSED=geschlossen ROTARY_HANDLE_SENSOR|MSG_FOR_POS_C|NO_MSG=keine Meldung ROTARY_HANDLE_SENSOR|MSG_FOR_POS_C|OPEN=offen ROTARY_HANDLE_SENSOR|MSG_FOR_POS_C|TILTED=gekippt @@ -967,58 +1247,66 @@ ROTARY_HANDLE_SENSOR|STATE|OPEN=Fensterzustand: offen ROTARY_HANDLE_SENSOR|STATE|TILTED=Fensterzustand: gekippt ROTARY_HANDLE_TRANSCEIVER=Fenster-Drehgriffkontakt ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_A=Meldung in Position unten -ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_A|CLOSED=zu +ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_A|CLOSED=geschlossen ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_A|NO_MSG=keine Meldung ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_A|OPEN=offen ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_A|TILTED=gekippt ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_B=Meldung in Position quer -ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_B|CLOSED=zu +ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_B|CLOSED=geschlossen ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_B|NO_MSG=keine Meldung ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_B|OPEN=offen ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_B|TILTED=gekippt ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_C=Meldung in Position oben -ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_C|CLOSED=zu +ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_C|CLOSED=geschlossen ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_C|NO_MSG=keine Meldung ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_C|OPEN=offen ROTARY_HANDLE_TRANSCEIVER|MSG_FOR_POS_C|TILTED=gekippt ROTARY_HANDLE_TRANSCEIVER|STATE|CLOSED=Fensterzustand: verriegelt ROTARY_HANDLE_TRANSCEIVER|STATE|OPEN=Fensterzustand: offen ROTARY_HANDLE_TRANSCEIVER|STATE|TILTED=Fensterzustand: gekippt -RSSI_DEVICE=RSSI Gert +ROUTER_MODULE_ENABLED=Gerät dient als Router +RSSI_DEVICE=RSSI Gerät RSSI_PEER=RSSI Partner SABOTAGE_MSG=Sabotagemeldung SECTION=Profilabschnitt: +SECTION_STATUS|NORMAL=Status Sektion: Normal +SECTION_STATUS|UNKNOWN=Status Sektion: Unbekannt +SELF_CALIBRATION_RESULT|FALSE=Kalibrierfahrt nicht erfolgreich +SELF_CALIBRATION_RESULT|TRUE=Kalibrierfahrt erfolgreich +SELF_CALIBRATION|START=Starte Kalibrierfahrt +SELF_CALIBRATION|STOP=Beende Kalibrierfahrt SENSOR_ERROR|FALSE=Sensor OK -SENSOR_ERROR|TRUE=Sensor gestrt -SENSOR_FOR_CARBON_DIOXIDE=Luftgtesensor +SENSOR_ERROR|TRUE=Sensor gestört +SENSOR_FOR_CARBON_DIOXIDE=Luftgütesensor SENSOR_FOR_CARBON_DIOXIDE|EVENT_FILTERTIME=Filterzeit SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_A=Meldung bei normaler CO2-Konzentation SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_A|LEVEL_NORMAL=CO2-Konzentration normal SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_A|NO_MSG=keine Meldung -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_B=Meldung bei erhhter CO2-Konzentation -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_B|LEVEL_ADDED=CO2-Konzentration erhht -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_B|LEVEL_ADDED_STRONG=CO2-Konzentration stark erhht +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_B=Meldung bei erhöhter CO2-Konzentation +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_B|LEVEL_ADDED=CO2-Konzentration erhöht +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_B|LEVEL_ADDED_STRONG=CO2-Konzentration stark erhöht SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_B|LEVEL_NORMAL=CO2-Konzentration normal SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_B|NO_MSG=keine Meldung -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_C=Meldung bei deutlich erhhter CO2-Konzentation -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_C|LEVEL_ADDED=CO2-Konzentration erhht -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_C|LEVEL_ADDED_STRONG=CO2-Konzentration stark erhht +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_C=Meldung bei deutlich erhöhter CO2-Konzentation +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_C|LEVEL_ADDED=CO2-Konzentration erhöht +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_C|LEVEL_ADDED_STRONG=CO2-Konzentration stark erhöht SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_C|LEVEL_NORMAL=CO2-Konzentration normal SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_C|NO_MSG=keine Meldung -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D=Meldung bei stark erhhter CO2-Konzentation -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D|LEVEL_ADDED=CO2-Konzentration erhht -SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D|LEVEL_ADDED_STRONG=CO2-Konzentration stark erhht +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D=Meldung bei stark erhöhter CO2-Konzentation +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D|LEVEL_ADDED=CO2-Konzentration erhöht +SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D|LEVEL_ADDED_STRONG=CO2-Konzentration stark erhöht SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D|LEVEL_NORMAL=CO2-Konzentration normal SENSOR_FOR_CARBON_DIOXIDE|MSG_FOR_POS_D|NO_MSG=keine Meldung -SENSOR_FOR_CARBON_DIOXIDE|STATE|LEVEL_ADDED=CO2-Konzentration erhht -SENSOR_FOR_CARBON_DIOXIDE|STATE|LEVEL_ADDED_STRONG=CO2-Konzentration stark erhht +SENSOR_FOR_CARBON_DIOXIDE|STATE|LEVEL_ADDED=CO2-Konzentration erhöht +SENSOR_FOR_CARBON_DIOXIDE|STATE|LEVEL_ADDED_STRONG=CO2-Konzentration stark erhöht SENSOR_FOR_CARBON_DIOXIDE|STATE|LEVEL_NORMAL=CO2-Konzentration normal +SENSOR_SENSITIVITY=Sensorempfindlichkeit SENSOR|FALSE=geschlossen SENSOR|INPUT_LOCKED=Eingang gesperrt SENSOR|TRUE=offen SET_TEMPERATURE=Soll-Temperatur -SHUTTER_CONTACT=Schlieerkontakt -SHUTTER_CONTACT_HMIP=Schlieerkontakt +SHUTTER_CONTACT=Schließerkontakt +SHUTTER_CONTACT_HMIP=Schließerkontakt SHUTTER_CONTACT_HMIP|ERROR|SABOTAGE=Sabotage SHUTTER_CONTACT_HMIP|MSG_FOR_POS_A=Meldung in Position offen SHUTTER_CONTACT_HMIP|MSG_FOR_POS_A|NO_MSG=keine Meldung @@ -1044,12 +1332,25 @@ SHUTTER_CONTACT|STATE|CLOSED=geschlossen SHUTTER_CONTACT|STATE|FALSE=geschlossen SHUTTER_CONTACT|STATE|OPEN=offen SHUTTER_CONTACT|STATE|TRUE=offen -SHUTTER_TRANSMITTER|LEVEL=Behanghhe +SHUTTER_TRANSMITTER|ACTIVITY_STATE|DOWN=Rolllade fährt herunter +SHUTTER_TRANSMITTER|ACTIVITY_STATE|STABLE=Rolllade steht +SHUTTER_TRANSMITTER|ACTIVITY_STATE|UNKNOWN=Rolllade Aktivität unbekannt +SHUTTER_TRANSMITTER|ACTIVITY_STATE|UP=Rolllade fährt hoch +SHUTTER_TRANSMITTER|LEVEL=Behanghöhe SHUTTER_TRANSMITTER|LEVEL_2=Lamellenposition -SHUTTER_VIRTUAL_RECEIVER|LEVEL=Behanghhe +SHUTTER_TRANSMITTER|PROCESS|NOT_STABLE=Rolllade fährt +SHUTTER_TRANSMITTER|PROCESS|STABLE=Rolllade steht +SHUTTER_VIRTUAL_RECEIVER=Rollladenaktor +SHUTTER_VIRTUAL_RECEIVER|ACTIVITY_STATE|DOWN=Rolllade fährt herunter +SHUTTER_VIRTUAL_RECEIVER|ACTIVITY_STATE|STABLE=Rolllade steht +SHUTTER_VIRTUAL_RECEIVER|ACTIVITY_STATE|UNKNOWN=Rolllade Aktivität unbekannt +SHUTTER_VIRTUAL_RECEIVER|ACTIVITY_STATE|UP=Rolllade fährt hoch +SHUTTER_VIRTUAL_RECEIVER|LEVEL=Behanghöhe SHUTTER_VIRTUAL_RECEIVER|LEVEL_2=Lamellenposition +SHUTTER_VIRTUAL_RECEIVER|PROCESS|NOT_STABLE=Rolllade fährt +SHUTTER_VIRTUAL_RECEIVER|PROCESS|STABLE=Rolllade steht SHUTTER_VIRTUAL_RECEIVER|STOP=Anhalten -SIGNAL=Besttigungston +SIGNAL=Bestätigungston SIGNAL_CHIME=Signalaktor (akustisch) SIGNAL_CHIME|ACT_NUM=Anzahl der Signale SIGNAL_CHIME|ACT_TYP=Art der Signale @@ -1062,15 +1363,16 @@ SIGNAL_LED|ACT_TYP=Art der Signale SIGNAL_LED|ON_TIME=Einschaltdauer SIGNAL_LED|STATE|FALSE=Aus SIGNAL_LED|STATE|TRUE=Ein -SIGNAL_TONE=Klang Besttigungston +SIGNAL_TONE=Klang Bestätigungston SIGNAL_TONE|HIGH=hoch SIGNAL_TONE|LOW=tief SIGNAL_TONE|MID=mittel SIGNAL_TONE|VERY_HIGH=sehr hoch +SIMPLE_SWITCH_RECEIVER=Schaltaktor SMOKE_DETECTOR_ALARM_STATUS|IDLE_OFF=Ruhezustand SMOKE_DETECTOR_ALARM_STATUS|INTRUSION_ALARM=Einbruch-Alarm SMOKE_DETECTOR_ALARM_STATUS|PRIMARY_ALARM=lokaler Alarm -SMOKE_DETECTOR_ALARM_STATUS|SECONDARY_ALARM=fremdausgelster Alarm +SMOKE_DETECTOR_ALARM_STATUS|SECONDARY_ALARM=fremdausgelöster Alarm SMOKE_DETECTOR_COMMAND|COMMUNICATION_TEST=Kommunikationstest SMOKE_DETECTOR_COMMAND|COMMUNICATION_TEST_REPEATED=Kommunikationstest (weitergeleitet) SMOKE_DETECTOR_COMMAND|INTRUSION_ALARM=Einbruch-Alarm ein @@ -1083,7 +1385,7 @@ SMOKE_DETECTOR_EVENT|INTRUSION_ALARM_REPEATED=Einbruch-Alarm (weitergeleitet) SMOKE_DETECTOR_EVENT|LOW_BAT=schwache Batterie SMOKE_DETECTOR_EVENT|LOW_BAT_REPEATED=schwache Batterie (weitergeleitet) SMOKE_DETECTOR_EVENT|PRIMARY_ALARM=lokaler Alarm -SMOKE_DETECTOR_EVENT|SECONDARY_ALARM=fremdausgelster Alarm +SMOKE_DETECTOR_EVENT|SECONDARY_ALARM=fremdausgelöster Alarm SMOKE_DETECTOR_TEAM=Rauchmelder SMOKE_DETECTOR_TEAM_V2=Rauchmelder SMOKE_DETECTOR_TEAM_V2|STATE|FALSE=Kein Rauch erkannt @@ -1103,6 +1405,10 @@ SMOKE_DETECTOR|REPEAT_ENABLE=Weiterleiten von empfangenen Datentelegrammen SMOKE_DETECTOR|STATE|FALSE=Kein Rauch erkannt SMOKE_DETECTOR|STATE|TRUE=Rauch erkannt SOFTONOFF=Soft On/Off +SOUNDFILE|DO_NOT_CARE=Mit dem aktuellen Titel fortfahren +SOUNDFILE|INTERNAL_SOUNDFILE=Interner Gerätesound +SOUNDFILE|OLD_VALUE=Zuletzt gespielter Titel +SOUNDFILE|RANDOM_SOUNDFILE=Zufallswiedergabe SOUND_ID=Alarmsignal SOUND_LONG=Lang SOUND_LONG_LONG=Lang / Lang @@ -1113,9 +1419,10 @@ SOUND_SHORT=Kurz SOUND_SHORT_SHORT=Kurz / Kurz SPEED_MULTIPLIER=Faktor PWM-Frequenz STANDBY_TIME=Zeit bis zum Standby-Modus +STATE_RESET_RECEIVER=Unterdrückung Bewegungserkennung STATE|FALSE=Schaltzustand: Aus STATE|TRUE=Schaltzustand: Ein -STATUSINFO_MINDELAY=Statusmeldungen Mindestverzgerung +STATUSINFO_MINDELAY=Statusmeldungen Mindestverzögerung STATUSINFO_RANDOM=Statusmeldungen Zufallsanteil STATUS_INDICATOR=Statusanzeige STATUS_INDICATOR|INHIBIT|FALSE=Sperrung inaktiv @@ -1123,13 +1430,25 @@ STATUS_INDICATOR|INHIBIT|TRUE=Sperrung aktiv STATUS_INDICATOR|ON_TIME=Einschaltdauer STATUS_INDICATOR|STATE|FALSE=Schaltzustand aus STATUS_INDICATOR|STATE|TRUE=Schaltzustand ein -STATUS_MESSAGE_TEXT_ALIGNMENT_LEFT_ALIGNED=Statusmeldung linksbndig darstellen +STATUS_MESSAGE_TEXT_ALIGNMENT_LEFT_ALIGNED=Statusmeldung linksbündig darstellen +STICKY_UNREACH|FALSE=Kommunikation war gestört: Nein +STICKY_UNREACH|TRUE=Kommunikation war gestört: Ja +STORM_LOWER_THRESHOLD=Windalarm-Ausschaltschwelle +STORM_UPPER_THRESHOLD=Windalarm-Einschaltschwelle SUBMIT=Kanalaktion +SUNSHINEDURATION=Sonnenscheindauer +SUNSHINEDURATION_OVERFLOW|FALSE=Zähler Sonnenschein Überlauf: Nein +SUNSHINEDURATION_OVERFLOW|TRUE=Zähler Sonnenschein Überlauf: Ja +SUNSHINE_THRESHOLD=Sonnenscheinschwelle +SUNSHINE_THRESHOLD_OVERRUN=Sonnenschein +SUNSHINE_THRESHOLD_OVERRUN|FALSE=aktuell kein Sonnenschein +SUNSHINE_THRESHOLD_OVERRUN|TRUE=aktuell Sonnenschein SWITCH=Schaltaktor +SWITCH_ACTUATOR=Schaltaktor SWITCH_INTERFACE=Schalter-Interface -SWITCH_INTERFACE|PRESS=bettigt -SWITCH_INTERFACE|STATE|FALSE=Schalterposition: unten gedrckt -SWITCH_INTERFACE|STATE|TRUE=Schalterposition: oben gedrckt +SWITCH_INTERFACE|PRESS=betätigt +SWITCH_INTERFACE|STATE|FALSE=Schalterposition: unten gedrückt +SWITCH_INTERFACE|STATE|TRUE=Schalterposition: oben gedrückt SWITCH_PANIC=Alarmsirene (Panikkanal) SWITCH_SENSOR=Alarmsirene SWITCH_TRANSMIT=Zweipunktregler Wandthermostat @@ -1142,6 +1461,7 @@ SWITCH|STATE|TRUE=Schaltzustand: ein SWITCH|STATUSINFO_RANDOM_A=Um Kollisionen beim Senden von Statusmeldungen zu + TACTILE_SWITCH|FALSE=Betriebsart Taster nicht aktiv TACTILE_SWITCH|TRUE=Betriebsart Taster aktiv +TEMPERATURE=Temperatur TEMPERATURE_COMFORT=Komfort-Temperatur TEMPERATURE_LIMITER|FALSE=Betriebsart Temperaturbegrenzung nicht aktiv TEMPERATURE_LIMITER|TRUE=Betriebsart Temperaturbegrenzung aktiv @@ -1149,6 +1469,10 @@ TEMPERATURE_LOWERING=Eco-Temperatur TEMPERATURE_MAXIMUM=Maximale Temperatur TEMPERATURE_MINIMUM=Minimale Temperatur TEMPERATURE_OFFSET=Temperatur-Offset +TEMPERATURE_OUT_OF_RANGE|FALSE=Umgebungstemperatur OK +TEMPERATURE_OUT_OF_RANGE|TRUE=Umgebungstemperatur unzulässing +TEMP_HUMIDITY_PARTICULATE_MATTER_TRANSMITTER|INTERVAL_UNIT=Einheit der automatischen Sensorreinigung +TEMP_HUMIDITY_PARTICULATE_MATTER_TRANSMITTER|INTERVAL_VALUE=Wert der automatischen Sensorreinigung THERMALCONTROL_TRANSMIT=Temperatursensor Wandthermostat TILT_SENSOR=Neigungssensor TILT_SENSOR|EVENT_FILTERTIME=Filterzeit @@ -1169,28 +1493,32 @@ TX_MINDELAY=Mindestsendeabstand TX_MINDELAY_UNIT=Einheit des Mindestsendeabstandes TX_MINDELAY_VALUE=Wert Mindestsendeabstand TX_THRESHOLD_POWER=TX Differenz Leistung -UNREACH|FALSE=Gertekommunikation OK -UNREACH|TRUE=Gertekommunikation gestrt +TYPICAL_PARTICLE_SIZE=Typische Partikelgröße +UNREACH|FALSE=Gerätekommunikation OK +UNREACH|TRUE=Gerätekommunikation gestört USER_COLOR=Kanalaktion USER_PROGRAM=Kanalaktion +VALVE_MAXIMUM_POSITION=maximale Ventilöffnungsposition VALVE_STATE=Ventilposition -VALVE_STATE|ADAPTION_DONE=Adaptionsfahrt durchgefhrt +VALVE_STATE|ADAPTION_DONE=Adaptionsfahrt durchgeführt VALVE_STATE|ADAPTION_IN_PROGRESS=Adaptionsfahrt aktiv -VALVE_STATE|ADJUSTMENT_TOO_BIG=Ventilstellbereich zu gro +VALVE_STATE|ADJUSTMENT_TOO_BIG=Ventilstellbereich zu groß VALVE_STATE|ADJUSTMENT_TOO_SMALL=Ventilstellbereich zu klein VALVE_STATE|ERROR_POSITION=Ventil in Fehlerposition -VALVE_STATE|RUN_TO_START=Ventil fhrt in Ausgangsposition (Ventil fhrt ganz auf) +VALVE_STATE|RUN_TO_START=Ventil fährt in Ausgangsposition (Ventil fährt ganz auf) VALVE_STATE|STATE_NOT_AVAILABLE=Ventilstatus unbekannt -VALVE_STATE|TOO_TIGHT=Ventil schwergngig / Ventil klemmt +VALVE_STATE|TOO_TIGHT=Ventil schwergängig / Ventil klemmt VALVE_STATE|WAIT_FOR_ADAPTION=Warte auf Adaptionsfahrt -VENT_CLOSED=Ventil schlieen -VENT_OPEN=Ventil ffnen +VENT_CLOSED=Ventil schließen +VENT_OPEN=Ventil öffnen +VIR-LG-ONOFF-CH|LEVEL|FALSE=Schaltzustand: Aus +VIR-LG-ONOFF-CH|LEVEL|TRUE=Schaltzustand: Ein VIRTUAL_DIMMER=Dimmaktor -VIRTUAL_DIMMER|ERROR_OVERHEAT=berhitzung -VIRTUAL_DIMMER|ERROR_OVERLOAD=berlastung +VIRTUAL_DIMMER|ERROR_OVERHEAT=Überhitzung +VIRTUAL_DIMMER|ERROR_OVERLOAD=Überlastung VIRTUAL_DIMMER|ERROR_REDUCED=Temperatur kritisch (Lastabsenkung) VIRTUAL_DIMMER|ERROR|LOAD_FAILURE=Lastfehler -VIRTUAL_DIMMER|LOGIC_COMBINATION=Verknpfungsregel +VIRTUAL_DIMMER|LOGIC_COMBINATION=Verknüpfungsregel VIRTUAL_DIMMER|POWERUP_ACTION=Aktion bei Spannungszufuhr VIRTUAL_DIMMER|RAMP_STOP=Stop Dimmrampe VIRTUAL_DIMMER|STATUSINFO_RANDOM=Statusmeldungen Zufallsanteil @@ -1198,27 +1526,29 @@ VIRTUAL_DUAL_WHITE_COLOR|LEVEL=Farbwert VIRTUAL_DUAL_WHITE_COLOR|LEVEL_REAL=Farbwert Realkanal VIRTUAL_DUAL_WHITE_COLOR|OLD_LEVEL=Letzter Wert VIRTUAL_DUAL_WHITE_COLOR|RAMP_STOP=Stop Farbwechsel -VIRTUAL_DUAL_WHITE_COLOR|RAMP_TIME=Rampenzeit fr Farbwechsel +VIRTUAL_DUAL_WHITE_COLOR|RAMP_TIME=Rampenzeit für Farbwechsel VIRTUAL_KEY=Virtuelle Fernbedienung VIRTUAL_KEY|LEVEL=Prozentwert senden -VOLTAGE_0=Wert (relativ) fr die Steuerspannung bei 0% -VOLTAGE_100=Wert (relativ) fr die Steuerspannung bei 100% -VOLUME_0=Lautstrke 0% -VOLUME_10=Lautstrke 10% -VOLUME_100=Lautstrke 100% -VOLUME_20=Lautstrke 20% -VOLUME_30=Lautstrke 30% -VOLUME_40=Lautstrke 40% -VOLUME_50=Lautstrke 50% -VOLUME_60=Lautstrke 60% -VOLUME_70=Lautstrke 70% -VOLUME_80=Lautstrke 80% -VOLUME_90=Lautstrke 90% +VOLTAGE=Spannung +VOLTAGE_0=Wert (relativ) für die Steuerspannung bei 0% +VOLTAGE_100=Wert (relativ) für die Steuerspannung bei 100% +VOLUME_0=Lautstärke 0% +VOLUME_10=Lautstärke 10% +VOLUME_100=Lautstärke 100% +VOLUME_20=Lautstärke 20% +VOLUME_30=Lautstärke 30% +VOLUME_40=Lautstärke 40% +VOLUME_50=Lautstärke 50% +VOLUME_60=Lautstärke 60% +VOLUME_70=Lautstärke 70% +VOLUME_80=Lautstärke 80% +VOLUME_90=Lautstärke 90% WAKEUP_BEHAVIOUR=Sofortige Reaktion auf Tastendruck ohne vorherige Kanalauswahl WAKEUP_BEHAVIOUR_STATUS_MSG_CONFIRMATION=Tastendruckauswertung Statusmeldung -WAKEUP_BEHAVIOUR_STATUS_MSG_RESISTANCE=Statusmeldung nur per CCU lschbar +WAKEUP_BEHAVIOUR_STATUS_MSG_RESISTANCE=Statusmeldung nur per CCU löschbar WAKEUP_BEHAVIOUR_STATUS_SIGNALIZATION_CONFIRMATION=Tastendruckauswertung Signalisierung WAKEUP_DEFAULT_CHANNEL=Initialer Kanal bei Aktivierung +WALLMOUNTED_THERMOSTAT=Wandthermostat WATERDETECTIONSENSOR=Wasserdetektor WATERDETECTIONSENSOR|EVENT_FILTERTIME=Filterzeit WATERDETECTIONSENSOR|MSG_FOR_POS_A=Trockenheit @@ -1236,31 +1566,35 @@ WATERDETECTIONSENSOR|MSG_FOR_POS_C|WET=Feuchtigkeit erkannt WATERDETECTIONSENSOR|STATE|DRY=Trocken WATERDETECTIONSENSOR|STATE|WATER=Wasserstand erkannt WATERDETECTIONSENSOR|STATE|WET=Feuchtigkeit erkannt +WATERLEVEL_DETECTED|FALSE=Wasserstand erkannt: Nein +WATERLEVEL_DETECTED|TRUE=Wasserstand erkannt: Ja +WATER_DETECTION_TRANSMITTER=Wassersensor +WATER_DETECTION_TRANSMITTER|ALARMSTATE|FALSE=Feuchtigkeit oder Wasserstand erkannt: Nein +WATER_DETECTION_TRANSMITTER|ALARMSTATE|TRUE=Feuchtigkeit oder Wasserstand erkannt: Ja WEATHER=Wettersensor -WEATHER_RECEIVER=Heizungsthermostat (Empfnger Wetterdaten) +WEATHER_RECEIVER=Heizungsthermostat (Empfänger Wetterdaten) +WEATHER_TRANSMIT|ALARMSTATE|FALSE=Feuchtigkeit oder Wasserstand erkannt: Nein +WEATHER_TRANSMIT|ALARMSTATE|TRUE=Feuchtigkeit oder Wasserstand erkannt: Ja WEATHER_TRANSMIT|HUMIDITY=Rel. Luftfeuchte WEATHER_TRANSMIT|TEMPERATURE=Temperatur -WEATHER|AIR_PRESSURE=Luftdruck -WEATHER|BRIGHTNESS=Helligkeit -WEATHER|HUMIDITY=Rel. Luftfeuchte -WEATHER|RAINING=Regen -WEATHER|RAINING|FALSE=aktuell kein Regen -WEATHER|RAINING|TRUE=aktuell Regen -WEATHER|RAIN_COUNTER=Regenmenge -WEATHER|STORM_LOWER_THRESHOLD=Windalarm-Ausschaltschwelle -WEATHER|STORM_UPPER_THRESHOLD=Windalarm-Einschaltschwelle -WEATHER|SUNSHINEDURATION=Sonnenscheindauer -WEATHER|SUNSHINE_THRESHOLD=Sonnenscheinschwelle -WEATHER|TEMPERATURE=Temperatur -WEATHER|WIND_DIRECTION=Windrichtung -WEATHER|WIND_DIRECTION_RANGE=Windrichtung Schwankungsbreite -WEATHER|WIND_SPEED=Windgeschwindigkeit -WEATHER|WIND_SPEED_RESULT_SOURCE=Art des Windgeschwindigkeitswertes -WEATHER|WIND_SPEED_RESULT_SOURCE|AVERAGE_VALUE=Mittelwert -WEATHER|WIND_SPEED_RESULT_SOURCE|MAX_VALUE=Maximalwert +WEEK_PROGRAM_CHANNEL_LOCKS=Kanäle im Auto-Modus +WEEK_PROGRAM_TARGET_CHANNEL_LOCKS=Kanäle für Modus Wochenprogramm (binär) +WEEK_PROGRAM_TARGET_CHANNEL_LOCK|AUTO_MODE_WITHOUT_RESET=Wochenprogramm: Auto ohne Reset +WEEK_PROGRAM_TARGET_CHANNEL_LOCK|AUTO_MODE_WITH_RESET=Wochenprogramm: Auto mit Reset (Reset ohne Funktion) +WEEK_PROGRAM_TARGET_CHANNEL_LOCK|MANU_MODE=Wochenprogramm: Manuell WHITE=Farbtemperatur WINDOW_STATE=Fensterstatus -WINDOW_SWITCH_RECEIVER=Heizkrperthermostat +WINDOW_SWITCH_RECEIVER=Heizkörperthermostat +WIND_DIR=Windrichtung +WIND_DIRECTION=Windrichtung +WIND_DIRECTION_RANGE=Windrichtung Schwankungsbreite +WIND_DIR_RANGE=Windrichtung Schwankungsbreite +WIND_SPEED=Windgeschwindigkeit +WIND_SPEED_RESULT_SOURCE=Art des Windgeschwindigkeitswertes +WIND_SPEED_RESULT_SOURCE|AVERAGE_VALUE=Mittelwert +WIND_SPEED_RESULT_SOURCE|MAX_VALUE=Maximalwert +WIND_THRESHOLD_OVERRUN|FALSE=Windschwelle nicht überschritten +WIND_THRESHOLD_OVERRUN|TRUE=Windschwelle überschritten WINMATIC=Fenster-Kippantrieb WINMATIC|ERROR|MOTOR_TILT_ERROR=Fehler Kippantrieb WINMATIC|ERROR|MOTOR_TURN_ERROR=Fehler Drehantrieb @@ -1269,11 +1603,11 @@ WINMATIC|LEVEL|LOCKED=Fenster verriegelt WINMATIC|MOUNT_SIDE=Montageseite WINMATIC|MOUNT_SIDE|LEFT=links WINMATIC|MOUNT_SIDE|RIGHT=rechts -WINMATIC|PULL_FORCE=Kraft Kippantrieb beim Schlieen -WINMATIC|PUSH_FORCE=Kraft Kippantrieb beim ffnen +WINMATIC|PULL_FORCE=Kraft Kippantrieb beim Schließen +WINMATIC|PUSH_FORCE=Kraft Kippantrieb beim Öffnen WINMATIC|RELOCK_DELAY=Automatisch verriegeln nach Zeit WINMATIC|RELOCK_DELAY|NOT_USED=Nicht automatisch verriegeln -WINMATIC|SPEED=Geschwindigkeit fr durch Zentralenbefehl ausgelste Aktion +WINMATIC|SPEED=Geschwindigkeit für durch Zentralenbefehl ausgelöste Aktion WINMATIC|STATE_UNCERTAIN|FALSE=Fensterzustand bekannt WINMATIC|STATE_UNCERTAIN|TRUE=Fensterzustand unbekannt WINMATIC|STOP=Anhalten diff --git a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/tclrega-scripts.xml b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/tclrega-scripts.xml index cd7f81feca5d4..3207115845917 100644 --- a/bundles/org.openhab.binding.homematic/src/main/resources/homematic/tclrega-scripts.xml +++ b/bundles/org.openhab.binding.homematic/src/main/resources/homematic/tclrega-scripts.xml @@ -192,7 +192,7 @@ if (objectFound) { else { dp_type = "BOOL"; } } if (dp_obj.ValueType() == 16) { - if (dp_obj.ValueSubType() == 0 || dp_obj.ValueSubType() == 27) { dp_type = "INTEGER"; } + if ((dp_obj.ValueSubType() == 0) || (dp_obj.ValueSubType() == 27)) { dp_type = "INTEGER"; } else { dp_type = "ENUM"; } } if (dp_obj.ValueType() == 4 ) { dp_type = "FLOAT"; } diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/DimmerItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/DimmerItemConverter.java index d91d6898a3d44..090cd7b11e212 100644 --- a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/DimmerItemConverter.java +++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/DimmerItemConverter.java @@ -92,7 +92,7 @@ public State toState(String string) { value = PercentType.ZERO.toBigDecimal(); } newState = new PercentType(value); - } catch (NumberFormatException e) { + } catch (IllegalArgumentException e) { // ignore } } diff --git a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/NumberItemConverter.java b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/NumberItemConverter.java index 1a6ecb07b231d..4f5d3f4039cc1 100644 --- a/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/NumberItemConverter.java +++ b/bundles/org.openhab.binding.http/src/main/java/org/openhab/binding/http/internal/converter/NumberItemConverter.java @@ -14,8 +14,6 @@ import java.util.function.Consumer; -import javax.measure.format.MeasurementParseException; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.http.internal.config.HttpChannelConfig; @@ -62,7 +60,7 @@ protected State toState(String value) { return new QuantityType<>(trimmedValue); } } - } catch (IllegalArgumentException | MeasurementParseException e) { + } catch (IllegalArgumentException e) { // finally failed } } diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/IAqualinkClient.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/IAqualinkClient.java index 53c36c0b03145..5f6a4f5f663ad 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/IAqualinkClient.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/IAqualinkClient.java @@ -31,12 +31,12 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; -import org.openhab.binding.iaqualink.internal.api.model.AccountInfo; -import org.openhab.binding.iaqualink.internal.api.model.Auxiliary; -import org.openhab.binding.iaqualink.internal.api.model.Device; -import org.openhab.binding.iaqualink.internal.api.model.Home; -import org.openhab.binding.iaqualink.internal.api.model.OneTouch; -import org.openhab.binding.iaqualink.internal.api.model.SignIn; +import org.openhab.binding.iaqualink.internal.api.dto.AccountInfo; +import org.openhab.binding.iaqualink.internal.api.dto.Auxiliary; +import org.openhab.binding.iaqualink.internal.api.dto.Device; +import org.openhab.binding.iaqualink.internal.api.dto.Home; +import org.openhab.binding.iaqualink.internal.api.dto.OneTouch; +import org.openhab.binding.iaqualink.internal.api.dto.SignIn; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,7 +73,8 @@ public class IAqualinkClient { private static final String HEADER_ACCEPT_LANGUAGE = "en-us"; private static final String HEADER_ACCEPT_ENCODING = "br, gzip, deflate"; - private static final String SUPPORT_URL = "https://support.iaqualink.com"; + private static final String AUTH_URL = "https://prod.zodiac-io.com/users/v1/login"; + private static final String DEVICES_URL = "https://r-api.iaqualink.net/devices.json"; private static final String IAQUALINK_BASE_URL = "https://p-api.iaqualink.net/v1/mobile/session.json"; private Gson gson = new GsonBuilder().registerTypeAdapter(Home.class, new HomeDeserializer()) @@ -113,8 +114,8 @@ public AccountInfo login(@Nullable String username, @Nullable String password, @ throws IOException, NotAuthorizedException { String signIn = gson.toJson(new SignIn(apiKey, username, password)).toString(); try { - ContentResponse response = httpClient.newRequest(SUPPORT_URL + "/users/sign_in.json") - .method(HttpMethod.POST).content(new StringContentProvider(signIn), "application/json").send(); + ContentResponse response = httpClient.newRequest(AUTH_URL).method(HttpMethod.POST) + .content(new StringContentProvider(signIn), "application/json").send(); if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) { throw new NotAuthorizedException(response.getReason()); } @@ -139,7 +140,7 @@ public AccountInfo login(@Nullable String username, @Nullable String password, @ */ public Device[] getDevices(@Nullable String apiKey, @Nullable String token, int id) throws IOException, NotAuthorizedException { - return getAqualinkObject(UriBuilder.fromUri(SUPPORT_URL + "/devices.json"). // + return getAqualinkObject(UriBuilder.fromUri(DEVICES_URL). // queryParam("api_key", apiKey). // queryParam("authentication_token", token). // queryParam("user_id", id).build(), Device[].class); diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/AccountInfo.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/AccountInfo.java similarity index 98% rename from bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/AccountInfo.java rename to bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/AccountInfo.java index 934524200c277..abb627ad4c288 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/AccountInfo.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/AccountInfo.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.iaqualink.internal.api.model; +package org.openhab.binding.iaqualink.internal.api.dto; /** * Account Info Object diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Auxiliary.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Auxiliary.java similarity index 96% rename from bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Auxiliary.java rename to bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Auxiliary.java index aa29a1dada7d2..2ac0373a247b7 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Auxiliary.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Auxiliary.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.iaqualink.internal.api.model; +package org.openhab.binding.iaqualink.internal.api.dto; /** * Auxiliary devices. diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Device.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Device.java similarity index 98% rename from bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Device.java rename to bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Device.java index f253c654692af..594c4fde2ad8d 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Device.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Device.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.iaqualink.internal.api.model; +package org.openhab.binding.iaqualink.internal.api.dto; /** * Device refers to a iAqualink Pool Controller. diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Home.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Home.java similarity index 98% rename from bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Home.java rename to bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Home.java index a66fba246c76b..fad89b1151cd8 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/Home.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/Home.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.iaqualink.internal.api.model; +package org.openhab.binding.iaqualink.internal.api.dto; import java.util.Map; diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/OneTouch.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/OneTouch.java similarity index 95% rename from bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/OneTouch.java rename to bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/OneTouch.java index f28bcbd2bd925..5c9eeb5019ab3 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/OneTouch.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/OneTouch.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.iaqualink.internal.api.model; +package org.openhab.binding.iaqualink.internal.api.dto; /** * OneTouch Macros. diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/SignIn.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/SignIn.java similarity index 95% rename from bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/SignIn.java rename to bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/SignIn.java index 29b6b75605e29..c6ca28591bb80 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/model/SignIn.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/api/dto/SignIn.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.iaqualink.internal.api.model; +package org.openhab.binding.iaqualink.internal.api.dto; /** * Object used to login to service. diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/config/IAqualinkConfiguration.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/config/IAqualinkConfiguration.java index b73bab60cbe1a..16e530c358bc2 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/config/IAqualinkConfiguration.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/config/IAqualinkConfiguration.java @@ -12,36 +12,39 @@ */ package org.openhab.binding.iaqualink.internal.config; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Configuration properties for connecting to a iAqualink Account * * @author Dan Cunningham - Initial contribution * */ +@NonNullByDefault public class IAqualinkConfiguration { /** * user to us when connecting to the account */ - public String userName; + public String userName = ""; /** * password to us when connecting to the account */ - public String password; + public String password = ""; /** * Option serialId of the pool controller to connect to, only useful if you have more then one controller */ - public String serialId; + public String serialId = ""; /** * fixed API key provided by iAqualink clients (Android, IOS) , unknown if this will change in the future. */ - public String apiKey; + public String apiKey = ""; /** * Rate we poll for new data */ - public int refresh; + public int refresh = 30; } diff --git a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/handler/IAqualinkHandler.java b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/handler/IAqualinkHandler.java index c928bd6d04606..50a4e189bf465 100644 --- a/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/handler/IAqualinkHandler.java +++ b/bundles/org.openhab.binding.iaqualink/src/main/java/org/openhab/binding/iaqualink/internal/handler/IAqualinkHandler.java @@ -38,11 +38,11 @@ import org.openhab.binding.iaqualink.internal.IAqualinkBindingConstants; import org.openhab.binding.iaqualink.internal.api.IAqualinkClient; import org.openhab.binding.iaqualink.internal.api.IAqualinkClient.NotAuthorizedException; -import org.openhab.binding.iaqualink.internal.api.model.AccountInfo; -import org.openhab.binding.iaqualink.internal.api.model.Auxiliary; -import org.openhab.binding.iaqualink.internal.api.model.Device; -import org.openhab.binding.iaqualink.internal.api.model.Home; -import org.openhab.binding.iaqualink.internal.api.model.OneTouch; +import org.openhab.binding.iaqualink.internal.api.dto.AccountInfo; +import org.openhab.binding.iaqualink.internal.api.dto.Auxiliary; +import org.openhab.binding.iaqualink.internal.api.dto.Device; +import org.openhab.binding.iaqualink.internal.api.dto.Home; +import org.openhab.binding.iaqualink.internal.api.dto.OneTouch; import org.openhab.binding.iaqualink.internal.config.IAqualinkConfiguration; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; @@ -457,7 +457,7 @@ private State toState(String name, @Nullable String type, @Nullable String value default: return StringType.valueOf(value); } - } catch (NumberFormatException e) { + } catch (IllegalArgumentException e) { return UnDefType.UNDEF; } } diff --git a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/handler/ICalendarHandler.java b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/handler/ICalendarHandler.java index 632a818f3a5e5..8dbf05d4c4a58 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/handler/ICalendarHandler.java +++ b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/handler/ICalendarHandler.java @@ -65,6 +65,7 @@ * @author Michael Wodniok - Initial contribution * @author Andrew Fiddian-Green - Support for Command Tags embedded in the Event description * @author Michael Wodniok - Added last_update-channel and additional needed handling of it + * @author Michael Wodniok - Changed calculation of Future for refresh of channels */ @NonNullByDefault public class ICalendarHandler extends BaseBridgeHandler implements CalendarUpdateListener { @@ -337,6 +338,7 @@ private void rescheduleCalendarStateUpdate() { return; } final Instant now = Instant.now(); + Instant nextRegularUpdate = null; if (currentCalendar.isEventPresent(now)) { final Event currentEvent = currentCalendar.getCurrentEvent(now); if (currentEvent == null) { @@ -344,32 +346,33 @@ private void rescheduleCalendarStateUpdate() { "Could not schedule next update of states, due to unexpected behaviour of calendar implementation."); return; } + nextRegularUpdate = currentEvent.end; + } + + final Event nextEvent = currentCalendar.getNextEvent(now); + final ICalendarConfiguration currentConfig = this.configuration; + if (currentConfig == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Something is broken, the configuration is not available."); + return; + } + if (nextEvent != null) { + if (nextRegularUpdate == null || nextEvent.start.isBefore(nextRegularUpdate)) { + nextRegularUpdate = nextEvent.start; + } + } + + if (nextRegularUpdate != null) { updateJobFuture = scheduler.schedule(() -> { ICalendarHandler.this.updateStates(); ICalendarHandler.this.rescheduleCalendarStateUpdate(); - }, currentEvent.end.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS); - logger.debug("Scheduled update in {} seconds", currentEvent.end.getEpochSecond() - now.getEpochSecond()); + }, nextRegularUpdate.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS); + logger.debug("Scheduled update in {} seconds", nextRegularUpdate.getEpochSecond() - now.getEpochSecond()); } else { - final Event nextEvent = currentCalendar.getNextEvent(now); - final ICalendarConfiguration currentConfig = this.configuration; - if (currentConfig == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Something is broken, the configuration is not available."); - return; - } - if (nextEvent == null) { - updateJobFuture = scheduler.schedule(() -> { - ICalendarHandler.this.rescheduleCalendarStateUpdate(); - }, 1L, TimeUnit.DAYS); - logger.debug("Scheduled reschedule in 1 day"); - } else { - updateJobFuture = scheduler.schedule(() -> { - ICalendarHandler.this.updateStates(); - ICalendarHandler.this.rescheduleCalendarStateUpdate(); - }, nextEvent.start.getEpochSecond() - now.getEpochSecond(), TimeUnit.SECONDS); - logger.debug("Scheduled update in {} seconds", nextEvent.start.getEpochSecond() - now.getEpochSecond()); - - } + updateJobFuture = scheduler.schedule(() -> { + ICalendarHandler.this.rescheduleCalendarStateUpdate(); + }, 1L, TimeUnit.DAYS); + logger.debug("Scheduled reschedule in 1 day"); } } diff --git a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendar.java b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendar.java index 4f6d23edba647..61d2f9a25184d 100644 --- a/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendar.java +++ b/bundles/org.openhab.binding.icalendar/src/main/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendar.java @@ -59,6 +59,8 @@ * @author Andrew Fiddian-Green - Methods getJustBegunEvents() & getJustEndedEvents() * @author Michael Wodniok - Extension for filtered events * @author Michael Wodniok - Added logic for events moved with "RECURRENCE-ID" (issue 9647) + * @author Michael Wodniok - Extended logic for defined behavior with parallel current events + * (issue 10808) */ @NonNullByDefault class BiweeklyPresentableCalendar extends AbstractPresentableCalendar { @@ -320,6 +322,8 @@ private void classifyEvents(Collection positiveEvents, Collection positiveEvents = new ArrayList(); classifyEvents(positiveEvents, negativeEvents); + VEventWPeriod earliestEndingEvent = null; + for (final VEvent currentEvent : positiveEvents) { final DateIterator startDates = this.getRecurredEventDateIterator(currentEvent); final Duration duration = getEventLength(currentEvent); @@ -333,7 +337,9 @@ private void classifyEvents(Collection positiveEvents, Collection positiveEvents, Collection> otherCommandTypes = Arrays.asList(DecimalType.class, - QuantityType.class, OnOffType.class, OpenClosedType.class, UpDownType.class, HSBType.class, + private static final List> otherCommandTypes = Arrays.asList(HSBType.class, + DecimalType.class, QuantityType.class, OnOffType.class, OpenClosedType.class, UpDownType.class, PlayPauseType.class, RewindFastforwardType.class, StringType.class); private static final List> percentCommandType = Arrays.asList(PercentType.class); diff --git a/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendarTest.java b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendarTest.java index 7bdb0e39778ec..7c60c4fb68f34 100644 --- a/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendarTest.java +++ b/bundles/org.openhab.binding.icalendar/src/test/java/org/openhab/binding/icalendar/internal/logic/BiweeklyPresentableCalendarTest.java @@ -41,13 +41,14 @@ * @author Michael Wodniok - Initial contribution. * @author Andrew Fiddian-Green - Tests for Command Tag code * @author Michael Wodniok - Extended Tests for filtered Events - * + * @author Michael Wodniok - Extended Test for parallel current events */ public class BiweeklyPresentableCalendarTest { private AbstractPresentableCalendar calendar; private AbstractPresentableCalendar calendar2; private AbstractPresentableCalendar calendar3; private AbstractPresentableCalendar calendar_issue9647; + private AbstractPresentableCalendar calendar_issue10808; @BeforeEach public void setUp() throws IOException, CalendarException { @@ -56,6 +57,8 @@ public void setUp() throws IOException, CalendarException { calendar3 = new BiweeklyPresentableCalendar(new FileInputStream("src/test/resources/test3.ics")); calendar_issue9647 = new BiweeklyPresentableCalendar( new FileInputStream("src/test/resources/test-issue9647.ics")); + calendar_issue10808 = new BiweeklyPresentableCalendar( + new FileInputStream("src/test/resources/test-issue10808.ics")); } /** @@ -117,6 +120,18 @@ public void testGetCurrentEvent() { Event nonExistingEvent = calendar.getCurrentEvent(Instant.parse("2019-09-09T09:07:00Z")); assertNull(nonExistingEvent); + + Event currentEvent2 = calendar_issue10808.getCurrentEvent(Instant.parse("2021-06-05T17:10:05Z")); + assertNotNull(currentEvent2); + assertTrue("Test event 1".contentEquals(currentEvent2.title)); + + Event currentEvent3 = calendar_issue10808.getCurrentEvent(Instant.parse("2021-06-05T17:13:05Z")); + assertNotNull(currentEvent3); + assertTrue("Test event 2".contentEquals(currentEvent3.title)); + + Event currentEvent4 = calendar_issue10808.getCurrentEvent(Instant.parse("2021-06-05T17:18:05Z")); + assertNotNull(currentEvent4); + assertTrue("Test event 1".contentEquals(currentEvent4.title)); } /** diff --git a/bundles/org.openhab.binding.icalendar/src/test/resources/test-issue10808.ics b/bundles/org.openhab.binding.icalendar/src/test/resources/test-issue10808.ics new file mode 100644 index 0000000000000..baadc321b053a --- /dev/null +++ b/bundles/org.openhab.binding.icalendar/src/test/resources/test-issue10808.ics @@ -0,0 +1,47 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//SabreDAV//SabreDAV//EN +X-WR-CALNAME:Test +X-APPLE-CALENDAR-COLOR:#499AA2 +REFRESH-INTERVAL;VALUE=DURATION:PT4H +X-PUBLISHED-TTL:PT4H +BEGIN:VTIMEZONE +TZID:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20210107T155730Z +DTSTAMP:20210107T160226Z +LAST-MODIFIED:20210605T163408Z +SEQUENCE:4 +UID:bd676ec9-0cdb-4c8d-a6dd-9e3fcf77a45f +DTSTART;TZID=Europe/Paris:20210605T191000 +DTEND;TZID=Europe/Paris:20210605T192000 +SUMMARY:Test event 1 +DESCRIPTION:BEGIN:DemoString:EHLO1 +END:VEVENT +BEGIN:VEVENT +CREATED:20210107T160405Z +DTSTAMP:20210107T160405Z +LAST-MODIFIED:20210605T163512Z +UID:7d1ecca5-1ddd-4932-b096-d034c8d7f5aa +DTSTART;TZID=Europe/Paris:20210605T191200 +DTEND;TZID=Europe/Paris:20210605T191600 +SUMMARY:Test event 2 +DESCRIPTION:BEGIN:DemoString:EHLO2 +END:VEVENT +END:VCALENDAR diff --git a/bundles/org.openhab.binding.ipcamera/README.md b/bundles/org.openhab.binding.ipcamera/README.md index 44ad00c73830d..9be185f4c7f12 100644 --- a/bundles/org.openhab.binding.ipcamera/README.md +++ b/bundles/org.openhab.binding.ipcamera/README.md @@ -115,7 +115,7 @@ Thing ipcamera:hikvision:West "West Camera" port=80, nvrChannel=4, serverPort=54324, - ffmpegOutput="/etc/openhab2/html/cameras/camera-west/", + ffmpegOutput="/var/lib/openhab/ipcamera/West/", ffmpegInput="rtsp://192.168.0.XX:554/ISAPI/Streaming/channels/401" ] ``` @@ -192,7 +192,7 @@ If you do not specify any of these, the binding will use the default which shoul | `ffmpegInput`| Best if this stream is in H.264 format and can be RTSP or HTTP URLs. Leave this blank to use the auto detected RTSP address for ONVIF cameras. | | `ffmpegInputOptions` | Allows you to specify any options before the -i on the commands for FFmpeg. If you have a ESP32 camera that only has a mjpeg stream then make this equal `-f mjpeg`. | | `ffmpegLocation`| The full path including the filename for where you have installed FFmpeg. The default should work for most Linux installs but if using windows use this format: `c:\ffmpeg\bin\ffmpeg.exe` | -| `ffmpegOutput`| The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this format: `c:\openhabconf\html\ipcamera\`. If you would like to expose the GIF files to your static server, you can set it to `/etc/openhab2/html/cameras/camera-name/` | +| `ffmpegOutput`| The full path to a unique folder (different for each camera) where FFmpeg has the ability to write files to ending with a slash. If you leave this blank, the binding will automatically use `$OPENHAB_USERDATA/ipcamera/UID`. See here for where this is located on your installation, | | `hlsOutOptions`| This gives you direct access to specify your own FFmpeg options to be used. Default: `-strict -2 -f lavfi -i aevalsrc=0 -acodec aac -vcodec copy -hls_flags delete_segments -hls_time 2 -hls_list_size 4` | | `gifOutOptions`| This gives you direct access to specify your own FFmpeg options to be used for animated GIF files. Default: `-r 2 -filter_complex scale=-2:360:flags=lanczos,setpts=0.5*PTS,split[o1][o2];[o1]palettegen[p];[o2]fifo[o3];[o3][p]paletteuse` | | `mjpegOptions` | Allows you to change the settings for creating a MJPEG stream from RTSP using FFmpeg. Possible reasons to change this would be to rotate or re-scale the picture from the camera, change the JPG compression for better quality or the FPS rate. | @@ -485,8 +485,7 @@ To use the HLS feature, you need to: + Ensure FFmpeg is installed. + For `generic` cameras, you will need to use the config `ffmpegInput` to provide a HTTP or RTSP URL. -+ Supply a folder that the openhab user has write permissions for to the config `ffmpegOutput`. -+ Set a valid `serverPort` as the default value of -1 will turn this feature off. ++ Set a valid `serverPort` as the value of -1 will turn this feature off. + Consider using a SSD/HDD, zram location, or a tmpfs (ram drive) can be used if you only have micro SD/flash based storage. ### Ram Drive Setup diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/CameraConfig.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/CameraConfig.java index 2f7f96afd83e2..df2e5725d7624 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/CameraConfig.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/CameraConfig.java @@ -94,6 +94,10 @@ public String getFfmpegOutput() { return ffmpegOutput; } + public void setFfmpegOutput(String path) { + ffmpegOutput = path; + } + public boolean getPtzContinuous() { return ptzContinuous; } diff --git a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java index ddc7a5d5929b0..5dd9481ecdc82 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java +++ b/bundles/org.openhab.binding.ipcamera/src/main/java/org/openhab/binding/ipcamera/internal/handler/IpCameraHandler.java @@ -57,6 +57,7 @@ import org.openhab.binding.ipcamera.internal.MyNettyAuthHandler; import org.openhab.binding.ipcamera.internal.StreamServerHandler; import org.openhab.binding.ipcamera.internal.onvif.OnvifConnection; +import org.openhab.core.OpenHAB; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; @@ -467,7 +468,7 @@ private String getCorrectUrlFormat(String longUrl) { String temp = longUrl; URL url; - if (longUrl.isEmpty() || longUrl.equals("ffmpeg")) { + if (longUrl.isEmpty() || "ffmpeg".equals(longUrl)) { return longUrl; } @@ -772,7 +773,7 @@ public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) { if (start) { if (mjpegChannelGroup.isEmpty()) {// first stream being requested. mjpegChannelGroup.add(ctx.channel()); - if (mjpegUri.isEmpty() || mjpegUri.equals("ffmpeg")) { + if (mjpegUri.isEmpty() || "ffmpeg".equals(mjpegUri)) { sendMjpegFirstPacket(ctx); setupFfmpegFormat(FFmpegFormat.MJPEG); } else { @@ -794,7 +795,7 @@ public void setupMjpegStreaming(boolean start, ChannelHandlerContext ctx) { mjpegChannelGroup.remove(ctx.channel()); if (mjpegChannelGroup.isEmpty()) { logger.debug("All ipcamera.mjpeg streams have stopped."); - if (mjpegUri.equals("ffmpeg") || mjpegUri.isEmpty()) { + if ("ffmpeg".equals(mjpegUri) || mjpegUri.isEmpty()) { Ffmpeg localMjpeg = ffmpegMjpeg; if (localMjpeg != null) { localMjpeg.stopConverting(); @@ -979,7 +980,7 @@ public void setupFfmpegFormat(FFmpegFormat format) { localGIF.startConverting(); if (gifHistory.isEmpty()) { gifHistory = gifFilename; - } else if (!gifFilename.equals("ipcamera")) { + } else if (!"ipcamera".equals(gifFilename)) { gifHistory = gifFilename + "," + gifHistory; if (gifHistoryLength > 49) { int endIndex = gifHistory.lastIndexOf(","); @@ -1003,7 +1004,7 @@ public void setupFfmpegFormat(FFmpegFormat format) { localRecord.startConverting(); if (mp4History.isEmpty()) { mp4History = mp4Filename; - } else if (!mp4Filename.equals("ipcamera")) { + } else if (!"ipcamera".equals(mp4Filename)) { mp4History = mp4Filename + "," + mp4History; if (mp4HistoryLength > 49) { int endIndex = mp4History.lastIndexOf(","); @@ -1193,6 +1194,26 @@ private void sendPTZRequest() { onvifCamera.sendPTZRequest(OnvifConnection.RequestType.AbsoluteMove); } + @Override + public void channelLinked(ChannelUID channelUID) { + if (cameraConfig.getServerPort() > 0) { + switch (channelUID.getId()) { + case CHANNEL_MJPEG_URL: + updateState(CHANNEL_MJPEG_URL, new StringType( + "http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.mjpeg")); + break; + case CHANNEL_HLS_URL: + updateState(CHANNEL_HLS_URL, + new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.m3u8")); + break; + case CHANNEL_IMAGE_URL: + updateState(CHANNEL_IMAGE_URL, + new StringType("http://" + hostIp + ":" + cameraConfig.getServerPort() + "/ipcamera.jpg")); + break; + } + } + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { @@ -1488,7 +1509,7 @@ void pollingCameraConnection() { if (rtspUri.isEmpty()) { logger.warn("Binding has not been supplied with a FFmpeg Input URL, so some features will not work."); } - if (snapshotUri.isEmpty() || snapshotUri.equals("ffmpeg")) { + if (snapshotUri.isEmpty() || "ffmpeg".equals(snapshotUri)) { snapshotIsFfmpeg(); } else { sendHttpRequest("GET", snapshotUri, null); @@ -1500,7 +1521,7 @@ void pollingCameraConnection() { cameraConfig.getOnvifPort()); onvifCamera.connect(thing.getThingTypeUID().getId().equals(ONVIF_THING)); } - if (snapshotUri.equals("ffmpeg")) { + if ("ffmpeg".equals(snapshotUri)) { snapshotIsFfmpeg(); } else if (!snapshotUri.isEmpty()) { sendHttpRequest("GET", snapshotUri, null); @@ -1664,6 +1685,10 @@ public void initialize() { snapshotUri = getCorrectUrlFormat(cameraConfig.getSnapshotUrl()); mjpegUri = getCorrectUrlFormat(cameraConfig.getMjpegUrl()); rtspUri = cameraConfig.getFfmpegInput(); + if (cameraConfig.getFfmpegOutput().isEmpty()) { + cameraConfig + .setFfmpegOutput(OpenHAB.getUserDataFolder() + "/ipcamera/" + this.thing.getUID().getId() + "/"); + } if (cameraConfig.getServerPort() < 1) { logger.warn( diff --git a/bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF/thing/thing-types.xml index 12624ebc1f1f1..576eeda0787db 100644 --- a/bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.ipcamera/src/main/resources/OH-INF/thing/thing-types.xml @@ -76,10 +76,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true @@ -218,10 +218,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true @@ -447,10 +447,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true @@ -699,10 +699,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true @@ -997,10 +997,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true @@ -1271,10 +1271,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true @@ -1542,10 +1542,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true @@ -1838,10 +1838,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true @@ -2121,10 +2121,10 @@ - The full path where FFmpeg has the ability to write files to ending with a slash. For windows use this - format, c:\openhabconf\html\ipcamera\ + Leave this blank and the binding will use the openHAB userdata folder. Alternatively, a unique path for + each camera that ends with a slash and has write permissions can be entered. - /etc/openhab/html/camera1/ + true diff --git a/bundles/org.openhab.binding.irobot/README.md b/bundles/org.openhab.binding.irobot/README.md index 5cf406fb1cdd0..902d2fdc6365b 100644 --- a/bundles/org.openhab.binding.irobot/README.md +++ b/bundles/org.openhab.binding.irobot/README.md @@ -1,32 +1,36 @@ # iRobot Binding -This binding provides integration of products by iRobot company (https://www.irobot.com/). It is currently developed to support Roomba 900 -series robotic vacuum cleaner with built-in Wi-Fi module. The binding interfaces to the robot directly without any need for a dedicated MQTT server. +This binding provides integration of products by iRobot company (https://www.irobot.com/). It is currently developed +to support Roomba vacuum cleaner/mopping robots with built-in Wi-Fi module. The binding interfaces to the robot directly +without any need for a dedicated MQTT server. ## Supported Things -- iRobot Roomba robotic vacuum cleaner (https://www.irobot.com/roomba). The binding has been developed and tested with Roomba 930. -- iRobot Braava has also been reported to (partially) work. Automatic configuration and password retrieval does not work. Add the robot manually as Roomba and use external tools (like Dorita980) in order to retrieve the password. +- iRobot Roomba robotic vacuum cleaner (https://www.irobot.com/roomba). +- iRobot Braava has also been reported to (partially) work. +- In general, the channel list is far from complete. There is a lot to do now. ## Discovery Roombas on the same network will be discovered automatically, however in order to connect to them a password is needed. The -password is a machine-generated string, which is unfortunately not exposed by the original iRobot smartphone application, but -it can be downloaded from the robot itself. If no password is configured, the Thing enters "CONFIGURATION PENDING" state. +password is a machine-generated string, which is unfortunately not exposed by the original iRobot smartphone application, +but it can be downloaded from the robot itself. If no password is configured, the Thing enters "CONFIGURATION PENDING" state. Now you need to perform authorization by pressing and holding the HOME button on your robot until it plays series of tones (approximately 2 seconds). The Wi-Fi indicator on the robot will flash for 30 seconds, the binding should automatically receive the password and go ONLINE. -After you've done this procedure you can write the password somewhere in case if you need to reconfigure your binding. It's not -known, however, whether the password is eternal or can change during factory reset. +After you've done this procedure you can write the password somewhere in case if you need to reconfigure your binding. It's +not known, however, whether the password is eternal or can change during factory reset. ## Thing Configuration +| Parameter | Type | Required | Default | Description | +| --------- | :-----: | :-------: | :------: | ----------------- | +| ipaddress | String | Yes | | Robot IP address | +| blid | String | No | | Robot ID | +| password | String | No | | Robot Password | -| Parameter | Meaning | -|-----------|----------------------------------------| -| ipaddress | IP address (or hostname) of your robot | -| password | Password for the robot | +All parameters will be autodiscovered. If using textual configuration, then `ipaddress` shall be specified. ## Channels @@ -140,11 +144,13 @@ Error codes. Data type is string in order to be able to utilize mapping to human | 76 | Hardware problem detected | ## Cleaning specific regions + You can clean one or many specific regions of a given map by sending the following String to the command channel: ``` cleanRegions:;,,.. ``` + The easiest way to determine the pmapId and region_ids is to monitor the last_command channel while starting a new mission for the specific region with the iRobot-App. ## Known Problems / Caveats diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/IRobotBindingConstants.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/IRobotBindingConstants.java index 4628b8ce28435..c6a7926b0ee38 100644 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/IRobotBindingConstants.java +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/IRobotBindingConstants.java @@ -12,7 +12,10 @@ */ package org.openhab.binding.irobot.internal; +import javax.net.ssl.TrustManager; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.io.net.http.TrustAllTrustManager; import org.openhab.core.thing.ThingTypeUID; /** @@ -21,6 +24,7 @@ * * @author hkuhn42 - Initial contribution * @author Pavel Fedin - rename and update + * @author Alexander Falkenstern - Add support for I7 series */ @NonNullByDefault public class IRobotBindingConstants { @@ -30,6 +34,9 @@ public class IRobotBindingConstants { // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_ROOMBA = new ThingTypeUID(BINDING_ID, "roomba"); + // Something goes wrong... + public static final String UNKNOWN = "UNKNOWN"; + // List of all Channel ids public static final String CHANNEL_COMMAND = "command"; public static final String CHANNEL_CYCLE = "cycle"; @@ -69,4 +76,12 @@ public class IRobotBindingConstants { public static final String PASSES_AUTO = "auto"; public static final String PASSES_1 = "1"; public static final String PASSES_2 = "2"; + + // Connection and config constants + public static final int MQTT_PORT = 8883; + public static final int UDP_PORT = 5678; + public static final TrustManager[] TRUST_MANAGERS = { TrustAllTrustManager.getInstance() }; + + public static final String ROBOT_BLID = "blid"; + public static final String ROBOT_PASSWORD = "password"; } diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/RawMQTT.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/RawMQTT.java deleted file mode 100644 index 5940f8fec7650..0000000000000 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/RawMQTT.java +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.irobot.internal; - -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * A "raw MQTT" client for sending custom "get password" request. - * Seems pretty much reinventing a bicycle, but it looks like HiveMq - * doesn't provide for sending and receiving custom packets. - * - * @author Pavel Fedin - Initial contribution - * - */ -@NonNullByDefault -public class RawMQTT { - public static final int ROOMBA_MQTT_PORT = 8883; - - private Socket socket; - - public static class Packet { - public byte message; - public byte[] payload; - - Packet(byte msg, byte[] data) { - message = msg; - payload = data; - } - - public boolean isValidPasswdPacket() { - return message == PasswdPacket.MESSAGE && payload.length >= PasswdPacket.HEADER_SIZE; - } - }; - - public static class PasswdPacket extends Packet { - static final byte MESSAGE = (byte) 0xF0; // MQTT Reserved - static final int MAGIC = 0x293bccef; - static final byte HEADER_SIZE = 5; - private ByteBuffer buffer; - - public PasswdPacket(Packet raw) { - super(raw.message, raw.payload); - buffer = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN); - } - - public int getMagic() { - return buffer.getInt(0); - } - - public byte getStatus() { - return buffer.get(4); - } - - public @Nullable String getPassword() { - if (getStatus() != 0) { - return null; - } - - int length = payload.length - HEADER_SIZE; - byte[] passwd = new byte[length]; - - buffer.position(HEADER_SIZE); - buffer.get(passwd); - - return new String(passwd, StandardCharsets.ISO_8859_1); - } - } - - // Roomba MQTT is using SSL with custom root CA certificate. - private static class MQTTTrustManager implements X509TrustManager { - @Override - public X509Certificate @Nullable [] getAcceptedIssuers() { - return null; - } - - @Override - public void checkClientTrusted(X509Certificate @Nullable [] arg0, @Nullable String arg1) - throws CertificateException { - } - - @Override - public void checkServerTrusted(X509Certificate @Nullable [] certs, @Nullable String authMethod) - throws CertificateException { - } - } - - public static TrustManager[] getTrustManagers() { - return new TrustManager[] { new MQTTTrustManager() }; - } - - public RawMQTT(InetAddress host, int port) throws KeyManagementException, NoSuchAlgorithmException, IOException { - SSLContext sc = SSLContext.getInstance("SSL"); - - sc.init(null, getTrustManagers(), new java.security.SecureRandom()); - socket = sc.getSocketFactory().createSocket(host, ROOMBA_MQTT_PORT); - socket.setSoTimeout(3000); - } - - public void close() throws IOException { - socket.close(); - } - - public void requestPassword() throws IOException { - final byte[] passwdRequest = new byte[7]; - ByteBuffer buffer = ByteBuffer.wrap(passwdRequest).order(ByteOrder.LITTLE_ENDIAN); - - buffer.put(PasswdPacket.MESSAGE); - buffer.put(PasswdPacket.HEADER_SIZE); - buffer.putInt(PasswdPacket.MAGIC); - buffer.put((byte) 0); - - socket.getOutputStream().write(passwdRequest); - } - - public @Nullable Packet readPacket() throws IOException { - byte[] header = new byte[2]; - int l = receive(header); - - if (l < header.length) { - return null; - } - - byte[] data = new byte[header[1]]; - l = receive(data); - - if (l != header[1]) { - return null; - } else { - return new Packet(header[0], data); - } - } - - private int receive(byte[] data) throws IOException { - int received = 0; - byte[] buffer = new byte[1024]; - InputStream in = socket.getInputStream(); - - while (received < data.length) { - int l = in.read(buffer); - - if (l <= 0) { - break; // EOF - } - - if (received + l > data.length) { - l = data.length - received; - } - - System.arraycopy(buffer, 0, data, received, l); - received += l; - } - - return received; - } -} diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/config/IRobotConfiguration.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/config/IRobotConfiguration.java new file mode 100644 index 0000000000000..c97f2d2790581 --- /dev/null +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/config/IRobotConfiguration.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2021 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.irobot.internal.config; + +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.UNKNOWN; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link IRobotConfiguration} is a class for IRobot thing configuration + * + * @author Pavel Fedin - Initial contribution + * @author Alexander Falkenstern - Add supported robot type + */ +@NonNullByDefault +public class IRobotConfiguration { + private String ipaddress = UNKNOWN; + private String password = UNKNOWN; + private String blid = UNKNOWN; + + public String getIpAddress() { + return ipaddress; + } + + public void setIpAddress(final String ipaddress) { + this.ipaddress = ipaddress.trim(); + } + + public String getPassword() { + return password.isBlank() ? UNKNOWN : password; + } + + public void setPassword(final String password) { + this.password = password; + } + + public String getBlid() { + return blid.isBlank() ? UNKNOWN : blid; + } + + public void setBlid(final String blid) { + this.blid = blid; + } +} diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/discovery/IRobotDiscoveryService.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/discovery/IRobotDiscoveryService.java index 46f52ee187152..9ea09e20a7668 100644 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/discovery/IRobotDiscoveryService.java +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/discovery/IRobotDiscoveryService.java @@ -12,25 +12,31 @@ */ package org.openhab.binding.irobot.internal.discovery; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.THING_TYPE_ROOMBA; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.UDP_PORT; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.UNKNOWN; + import java.io.IOException; +import java.io.StringReader; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +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.openhab.binding.irobot.internal.IRobotBindingConstants; -import org.openhab.binding.irobot.internal.dto.IdentProtocol; -import org.openhab.binding.irobot.internal.dto.IdentProtocol.IdentData; +import org.openhab.binding.irobot.internal.dto.MQTTProtocol.DiscoveryResponse; +import org.openhab.binding.irobot.internal.utils.LoginRequester; 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.config.discovery.DiscoveryService; import org.openhab.core.net.NetUtil; @@ -39,24 +45,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.JsonParseException; +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; /** - * Discovery service for iRobots + * Discovery service for iRobots. The {@link LoginRequester#getBlid} and + * {@link IRobotDiscoveryService} are heavily related to each other. * * @author Pavel Fedin - Initial contribution + * @author Alexander Falkenstern - Add support for I7 series * */ -@Component(service = DiscoveryService.class, configurationPid = "discovery.irobot") @NonNullByDefault +@Component(service = DiscoveryService.class, configurationPid = "discovery.irobot") public class IRobotDiscoveryService extends AbstractDiscoveryService { private final Logger logger = LoggerFactory.getLogger(IRobotDiscoveryService.class); + + private final Gson gson = new Gson(); + private final Runnable scanner; private @Nullable ScheduledFuture backgroundFuture; public IRobotDiscoveryService() { - super(Collections.singleton(IRobotBindingConstants.THING_TYPE_ROOMBA), 30, true); + super(Collections.singleton(THING_TYPE_ROOMBA), 30, true); + scanner = createScanner(); } @@ -88,18 +101,48 @@ protected void startScan() { private Runnable createScanner() { return () -> { + Set robots = new HashSet<>(); long timestampOfLastScan = getTimestampOfLastScan(); for (InetAddress broadcastAddress : getBroadcastAddresses()) { logger.debug("Starting broadcast for {}", broadcastAddress.toString()); - try (DatagramSocket socket = IdentProtocol.sendRequest(broadcastAddress)) { - DatagramPacket incomingPacket; + final byte[] bRequest = "irobotmcs".getBytes(StandardCharsets.UTF_8); + DatagramPacket request = new DatagramPacket(bRequest, bRequest.length, broadcastAddress, UDP_PORT); + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(1000); // One second + socket.setReuseAddress(true); + socket.setBroadcast(true); + socket.send(request); + + byte @Nullable [] reply = null; + while ((reply = receive(socket)) != null) { + robots.add(new String(reply, StandardCharsets.UTF_8)); + } + } catch (IOException exception) { + logger.debug("Error sending broadcast: {}", exception.toString()); + } + } - while ((incomingPacket = receivePacket(socket)) != null) { - discover(incomingPacket); + for (final String json : robots) { + + JsonReader jsonReader = new JsonReader(new StringReader(json)); + DiscoveryResponse msg = gson.fromJson(jsonReader, DiscoveryResponse.class); + + // Only firmware version 2 and above are supported via MQTT, therefore check it + if ((msg.ver != null) && (Integer.parseInt(msg.ver) > 1) && "mqtt".equalsIgnoreCase(msg.proto)) { + final String address = msg.ip; + final String mac = msg.mac; + final String sku = msg.sku; + if (!address.isEmpty() && !sku.isEmpty() && !mac.isEmpty()) { + ThingUID thingUID = new ThingUID(THING_TYPE_ROOMBA, mac.replace(":", "")); + DiscoveryResultBuilder builder = DiscoveryResultBuilder.create(thingUID); + builder = builder.withProperty("mac", mac).withRepresentationProperty("mac"); + builder = builder.withProperty("ipaddress", address); + + String name = msg.robotname; + builder = builder.withLabel("iRobot " + (!name.isEmpty() ? name : UNKNOWN)); + thingDiscovered(builder.build()); } - } catch (IOException e) { - logger.warn("Error sending broadcast: {}", e.toString()); } } @@ -107,59 +150,31 @@ private Runnable createScanner() { }; } + private byte @Nullable [] receive(DatagramSocket socket) { + try { + final byte[] bReply = new byte[1024]; + DatagramPacket reply = new DatagramPacket(bReply, bReply.length); + socket.receive(reply); + return Arrays.copyOfRange(reply.getData(), reply.getOffset(), reply.getLength()); + } catch (IOException exception) { + // This is not really an error, eventually we get a timeout due to a loop in the caller + return null; + } + } + private List getBroadcastAddresses() { ArrayList addresses = new ArrayList<>(); for (String broadcastAddress : NetUtil.getAllBroadcastAddresses()) { try { addresses.add(InetAddress.getByName(broadcastAddress)); - } catch (UnknownHostException e) { + } catch (UnknownHostException exception) { // The broadcastAddress is supposed to be raw IP, not a hostname, like 192.168.0.255. // Getting UnknownHost on it would be totally strange, some internal system error. - logger.warn("Error broadcasting to {}: {}", broadcastAddress, e.getMessage()); + logger.warn("Error broadcasting to {}: {}", broadcastAddress, exception.getMessage()); } } return addresses; } - - private @Nullable DatagramPacket receivePacket(DatagramSocket socket) { - try { - return IdentProtocol.receiveResponse(socket); - } catch (IOException e) { - // This is not really an error, eventually we get a timeout - // due to a loop in the caller - return null; - } - } - - private void discover(DatagramPacket incomingPacket) { - String host = incomingPacket.getAddress().toString().substring(1); - String reply = new String(incomingPacket.getData(), StandardCharsets.UTF_8); - - logger.trace("Received IDENT from {}: {}", host, reply); - - IdentProtocol.IdentData ident; - - try { - ident = IdentProtocol.decodeResponse(reply); - } catch (JsonParseException e) { - logger.warn("Malformed IDENT reply from {}!", host); - return; - } - - // This check comes from Roomba980-Python - if (ident.ver < IdentData.MIN_SUPPORTED_VERSION) { - logger.warn("Found unsupported iRobot \"{}\" version {} at {}", ident.robotname, ident.ver, host); - return; - } - - if (ident.product.equals(IdentData.PRODUCT_ROOMBA)) { - ThingUID thingUID = new ThingUID(IRobotBindingConstants.THING_TYPE_ROOMBA, host.replace('.', '_')); - DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperty("ipaddress", host) - .withRepresentationProperty("ipaddress").withLabel("iRobot " + ident.robotname).build(); - - thingDiscovered(result); - } - } } diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/IdentProtocol.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/IdentProtocol.java deleted file mode 100644 index fa822fd5abee4..0000000000000 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/IdentProtocol.java +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.irobot.internal.dto; - -import java.io.IOException; -import java.io.StringReader; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.nio.charset.StandardCharsets; - -import com.google.gson.Gson; -import com.google.gson.JsonParseException; -import com.google.gson.stream.JsonReader; - -/** - * iRobot discovery and identification protocol - * - * @author Pavel Fedin - Initial contribution - * - */ -public class IdentProtocol { - private static final String UDP_PACKET_CONTENTS = "irobotmcs"; - private static final int REMOTE_UDP_PORT = 5678; - private static final Gson gson = new Gson(); - - public static DatagramSocket sendRequest(InetAddress host) throws IOException { - DatagramSocket socket = new DatagramSocket(); - - socket.setBroadcast(true); - socket.setReuseAddress(true); - - byte[] packetContents = UDP_PACKET_CONTENTS.getBytes(StandardCharsets.UTF_8); - DatagramPacket packet = new DatagramPacket(packetContents, packetContents.length, host, REMOTE_UDP_PORT); - - socket.send(packet); - return socket; - } - - public static DatagramPacket receiveResponse(DatagramSocket socket) throws IOException { - byte[] buffer = new byte[1024]; - DatagramPacket incomingPacket = new DatagramPacket(buffer, buffer.length); - - socket.setSoTimeout(1000 /* one second */); - socket.receive(incomingPacket); - - return incomingPacket; - } - - public static IdentData decodeResponse(DatagramPacket packet) throws JsonParseException { - return decodeResponse(new String(packet.getData(), StandardCharsets.UTF_8)); - } - - public static IdentData decodeResponse(String reply) throws JsonParseException { - /* - * packet is a JSON of the following contents (addresses are undisclosed): - * @formatter:off - * { - * "ver":"3", - * "hostname":"Roomba-3168820480607740", - * "robotname":"Roomba", - * "ip":"XXX.XXX.XXX.XXX", - * "mac":"XX:XX:XX:XX:XX:XX", - * "sw":"v2.4.6-3", - * "sku":"R981040", - * "nc":0, - * "proto":"mqtt", - * "cap":{ - * "pose":1, - * "ota":2, - * "multiPass":2, - * "carpetBoost":1, - * "pp":1, - * "binFullDetect":1, - * "langOta":1, - * "maps":1, - * "edge":1, - * "eco":1, - * "svcConf":1 - * } - * } - * @formatter:on - */ - // We are not consuming all the fields, so we have to create the reader explicitly - // If we use fromJson(String) or fromJson(java.util.reader), it will throw - // "JSON not fully consumed" exception, because not all the reader's content has been - // used up. We want to avoid that for compatibility reasons because newer iRobot versions - // may add fields. - JsonReader jsonReader = new JsonReader(new StringReader(reply)); - IdentData data = gson.fromJson(jsonReader, IdentData.class); - - data.postParse(); - return data; - } - - public static class IdentData { - public static int MIN_SUPPORTED_VERSION = 2; - public static String PRODUCT_ROOMBA = "Roomba"; - - public int ver; - private String hostname; - public String robotname; - - // These two fields are synthetic, they are not contained in JSON - public String product; - public String blid; - - public void postParse() { - // Synthesize missing properties. - String[] hostparts = hostname.split("-"); - - // This also comes from Roomba980-Python. Comments there say that "iRobot" - // prefix is used by i7. We assume for other robots it would be product - // name, e. g. "Scooba" - if (hostparts[0].equals("iRobot")) { - product = "Roomba"; - } else { - product = hostparts[0]; - } - - blid = hostparts[1]; - } - } -} diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/MQTTProtocol.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/MQTTProtocol.java index db71ca99cf89d..fd9103590756f 100644 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/MQTTProtocol.java +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/dto/MQTTProtocol.java @@ -17,6 +17,7 @@ import java.util.stream.Collectors; import com.google.gson.JsonElement; +import com.google.gson.annotations.SerializedName; /** * iRobot MQTT protocol messages @@ -32,22 +33,24 @@ public interface Request { public static class CleanRoomsRequest extends CommandRequest { public int ordered; - public String pmap_id; + @SerializedName("pmap_id") + public String pmapId; public List regions; public CleanRoomsRequest(String cmd, String mapId, String[] regions) { super(cmd); ordered = 1; - pmap_id = mapId; + pmapId = mapId; this.regions = Arrays.stream(regions).map(i -> new Region(i)).collect(Collectors.toList()); } public static class Region { - public String region_id; + @SerializedName("region_id") + public String regionId; public String type; public Region(String id) { - this.region_id = id; + this.regionId = id; this.type = "rid"; } } @@ -262,4 +265,69 @@ public static class ReportedState { public static class StateMessage { public ReportedState state; } + + // DISCOVERY + public static class RobotCapabilities { + public Integer pose; + public Integer ota; + public Integer multiPass; + public Integer carpetBoost; + public Integer pp; + public Integer binFullDetect; + public Integer langOta; + public Integer maps; + public Integer edge; + public Integer eco; + public Integer scvConf; + } + + /* + * JSON of the following contents (addresses are undisclosed): + * @formatter:off + * { + * "ver":"3", + * "hostname":"Roomba-", + * "robotname":"Roomba", + * "robotid":"", --> available on some models only + * "ip":"XXX.XXX.XXX.XXX", + * "mac":"XX:XX:XX:XX:XX:XX", + * "sw":"v2.4.6-3", + * "sku":"R981040", + * "nc":0, + * "proto":"mqtt", + * "cap":{ + * "pose":1, + * "ota":2, + * "multiPass":2, + * "carpetBoost":1, + * "pp":1, + * "binFullDetect":1, + * "langOta":1, + * "maps":1, + * "edge":1, + * "eco":1, + * "svcConf":1 + * } + * } + * @formatter:on + */ + public static class DiscoveryResponse { + public String ver; + public String hostname; + public String robotname; + public String robotid; + public String ip; + public String mac; + public String sw; + public String sku; + public String nc; + public String proto; + public RobotCapabilities cap; + } + + // LoginRequester + public static class BlidResponse { + public String robotid; + public String hostname; + } }; diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/IRobotConnectionHandler.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/IRobotConnectionHandler.java new file mode 100644 index 0000000000000..253610355599d --- /dev/null +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/IRobotConnectionHandler.java @@ -0,0 +1,178 @@ +/** + * Copyright (c) 2010-2021 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.irobot.internal.handler; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.MQTT_PORT; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.TRUST_MANAGERS; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; +import org.openhab.core.io.transport.mqtt.MqttConnectionState; +import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; +import org.openhab.core.io.transport.mqtt.reconnect.PeriodicReconnectStrategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link IRobotConnectionHandler} is responsible for handling iRobot MQTT connection. + * + * @author hkuhn42 - Initial contribution + * @author Pavel Fedin - Rewrite for 900 series + * @author Alexander Falkenstern - Add support for I7 series + */ +@NonNullByDefault +public abstract class IRobotConnectionHandler implements MqttConnectionObserver, MqttMessageSubscriber { + private final Logger logger = LoggerFactory.getLogger(IRobotConnectionHandler.class); + + private static final int RECONNECT_DELAY = 10000; // In milliseconds + private @Nullable Future reconnect; + private @Nullable MqttBrokerConnection connection; + + public IRobotConnectionHandler() { + } + + public synchronized void connect(final String ip, final String blid, final String password) { + InetAddress host = null; + try { + host = InetAddress.getByName(ip); + } catch (UnknownHostException exception) { + connectionStateChanged(MqttConnectionState.DISCONNECTED, exception); + return; + } + + try { + boolean reachable = host.isReachable(1000); + if (logger.isTraceEnabled()) { + logger.trace("Connection to {} can be established {}", ip, reachable); + } + } catch (IOException exception) { + connectionStateChanged(MqttConnectionState.DISCONNECTED, exception); + return; + } + + // BLID is used as both client ID and username. The name of BLID also came from Roomba980-python + MqttBrokerConnection connection = new MqttBrokerConnection(ip, MQTT_PORT, true, blid); + + // Disable sending UNSUBSCRIBE request before disconnecting because Roomba doesn't like it. + // It just swallows the request and never sends any response, so stop() method never completes. + connection.setUnsubscribeOnStop(false); + connection.setCredentials(blid, password); + connection.setTrustManagers(TRUST_MANAGERS); + + // Roomba accepts MQTT qos 0 (AT_MOST_ONCE) only. + connection.setQos(0); + + // MQTT connection reconnects itself, so we don't have to reconnect, when it breaks + connection.setReconnectStrategy(new PeriodicReconnectStrategy(RECONNECT_DELAY, RECONNECT_DELAY)); + + connection.start().exceptionally(exception -> { + connectionStateChanged(MqttConnectionState.DISCONNECTED, exception); + return false; + }).thenAccept(successful -> { + MqttConnectionState state = successful ? MqttConnectionState.CONNECTED : MqttConnectionState.DISCONNECTED; + connectionStateChanged(state, successful ? null : new TimeoutException("Timeout")); + }); + + this.connection = connection; + } + + public synchronized void disconnect() { + Future reconnect = this.reconnect; + if (reconnect != null) { + reconnect.cancel(false); + this.reconnect = null; + } + + MqttBrokerConnection connection = this.connection; + if (connection != null) { + connection.unsubscribe("#", this); + CompletableFuture future = connection.stop(); + try { + future.get(10, TimeUnit.SECONDS); + if (logger.isTraceEnabled()) { + logger.trace("MQTT disconnect successful"); + } + } catch (InterruptedException | ExecutionException | TimeoutException exception) { + logger.warn("MQTT disconnect failed: {}", exception.getMessage()); + } + this.connection = null; + } + } + + @Override + public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) { + if (state == MqttConnectionState.CONNECTED) { + MqttBrokerConnection connection = this.connection; + + // This would be very strange, but Eclipse forces us to do the check + if (connection != null) { + reconnect = null; + + // Roomba sends us two topics: + // "wifistat" - reports signal strength and current robot position + // "$aws/things//shadow/update" - the rest of messages + // Subscribe to everything since we're interested in both + connection.subscribe("#", this).exceptionally(exception -> { + logger.warn("MQTT subscription failed: {}", exception.getMessage()); + return false; + }).thenAccept(successful -> { + if (successful && logger.isTraceEnabled()) { + logger.trace("MQTT subscription successful"); + } else { + logger.warn("MQTT subscription failed: Timeout"); + } + }); + } else { + logger.warn("Established connection without broker pointer"); + } + } else { + String message = (error != null) ? error.getMessage() : "Unknown reason"; + logger.warn("MQTT connection failed: {}", message); + } + } + + @Override + public void processMessage(String topic, byte[] payload) { + // Report raw JSON reply + final String json = new String(payload, UTF_8); + if (logger.isTraceEnabled()) { + logger.trace("Got topic {} data {}", topic, json); + } + + receive(topic, json); + } + + public abstract void receive(final String topic, final String json); + + public void send(final String topic, final String payload) { + MqttBrokerConnection connection = this.connection; + if (connection != null) { + if (logger.isTraceEnabled()) { + logger.trace("Sending {}: {}", topic, payload); + } + connection.publish(topic, payload.getBytes(UTF_8), connection.getQos(), false); + } + } +} diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/RoombaHandler.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/RoombaHandler.java index 7ef18956b9843..78d7f4e1ba75b 100644 --- a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/RoombaHandler.java +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/handler/RoombaHandler.java @@ -12,34 +12,59 @@ */ package org.openhab.binding.irobot.internal.handler; -import static org.openhab.binding.irobot.internal.IRobotBindingConstants.*; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.BIN_FULL; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.BIN_OK; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.BIN_REMOVED; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.BOOST_AUTO; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.BOOST_ECO; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.BOOST_PERFORMANCE; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_ALWAYS_FINISH; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_BATTERY; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_BIN; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_CLEAN_PASSES; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_COMMAND; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_CYCLE; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_EDGE_CLEAN; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_ERROR; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_LAST_COMMAND; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_MAP_UPLOAD; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_PHASE; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_POWER_BOOST; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_RSSI; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_SCHEDULE; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_SCHED_SWITCH; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_SCHED_SWITCH_PREFIX; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CHANNEL_SNR; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CMD_CLEAN; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CMD_CLEAN_REGIONS; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CMD_DOCK; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CMD_PAUSE; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.CMD_STOP; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.PASSES_1; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.PASSES_2; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.PASSES_AUTO; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.ROBOT_BLID; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.ROBOT_PASSWORD; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.UNKNOWN; +import static org.openhab.core.thing.ThingStatus.INITIALIZING; +import static org.openhab.core.thing.ThingStatus.OFFLINE; +import static org.openhab.core.thing.ThingStatus.UNINITIALIZED; import java.io.IOException; import java.io.StringReader; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.Hashtable; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.regex.Pattern; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.irobot.internal.RawMQTT; -import org.openhab.binding.irobot.internal.RoombaConfiguration; -import org.openhab.binding.irobot.internal.dto.IdentProtocol; -import org.openhab.binding.irobot.internal.dto.IdentProtocol.IdentData; +import org.openhab.binding.irobot.internal.config.IRobotConfiguration; import org.openhab.binding.irobot.internal.dto.MQTTProtocol; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; -import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; +import org.openhab.binding.irobot.internal.utils.LoginRequester; import org.openhab.core.io.transport.mqtt.MqttConnectionState; -import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; -import org.openhab.core.io.transport.mqtt.reconnect.PeriodicReconnectStrategy; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; @@ -67,18 +92,14 @@ * @author hkuhn42 - Initial contribution * @author Pavel Fedin - Rewrite for 900 series * @author Florian Binder - added cleanRegions command and lastCommand channel + * @author Alexander Falkenstern - Add support for I7 series */ @NonNullByDefault -public class RoombaHandler extends BaseThingHandler implements MqttConnectionObserver, MqttMessageSubscriber { +public class RoombaHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(RoombaHandler.class); + private final Gson gson = new Gson(); - private static final int RECONNECT_DELAY_SEC = 5; // In seconds - private @Nullable Future reconnectReq; - // Dummy RoombaConfiguration object in order to shut up Eclipse warnings - // The real one is set in initialize() - private RoombaConfiguration config = new RoombaConfiguration(); - private @Nullable String blid = null; - private @Nullable MqttBrokerConnection connection; + private Hashtable lastState = new Hashtable<>(); private MQTTProtocol.@Nullable Schedule lastSchedule = null; private boolean autoPasses = true; @@ -87,20 +108,51 @@ public class RoombaHandler extends BaseThingHandler implements MqttConnectionObs private @Nullable Boolean vacHigh = null; private boolean isPaused = false; + private @Nullable Future credentialRequester; + protected IRobotConnectionHandler connection = new IRobotConnectionHandler() { + @Override + public void receive(final String topic, final String json) { + RoombaHandler.this.receive(topic, json); + } + + @Override + public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) { + super.connectionStateChanged(state, error); + if (state == MqttConnectionState.CONNECTED) { + updateStatus(ThingStatus.ONLINE); + } else { + String message = (error != null) ? error.getMessage() : "Unknown reason"; + updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + } + } + }; + public RoombaHandler(Thing thing) { super(thing); } @Override public void initialize() { - config = getConfigAs(RoombaConfiguration.class); - updateStatus(ThingStatus.UNKNOWN); - scheduler.execute(this::connect); + IRobotConfiguration config = getConfigAs(IRobotConfiguration.class); + + if (UNKNOWN.equals(config.getPassword()) || UNKNOWN.equals(config.getBlid())) { + final String message = "Robot authentication is required"; + updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message); + scheduler.execute(this::getCredentials); + } else { + scheduler.execute(this::connect); + } } @Override public void dispose() { - scheduler.execute(this::disconnect); + Future requester = credentialRequester; + if (requester != null) { + requester.cancel(false); + credentialRequester = null; + } + + scheduler.execute(connection::disconnect); } // lastState.get() can return null if the key is not found according @@ -139,15 +191,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { String mapId = params[0]; String[] regionIds = params[1].split(","); - sendRequest(new MQTTProtocol.CleanRoomsRequest("start", mapId, regionIds)); + MQTTProtocol.Request request = new MQTTProtocol.CleanRoomsRequest("start", mapId, regionIds); + connection.send(request.getTopic(), gson.toJson(request)); } else { logger.warn("Invalid request: {}", cmd); logger.warn("Correct format: cleanRegions:;,,...>"); } } else { - sendRequest(new MQTTProtocol.CommandRequest(cmd)); + MQTTProtocol.Request request = new MQTTProtocol.CommandRequest(cmd); + connection.send(request.getTopic(), gson.toJson(request)); } - } } else if (ch.startsWith(CHANNEL_SCHED_SWITCH_PREFIX)) { MQTTProtocol.Schedule schedule = lastSchedule; @@ -205,166 +258,82 @@ private void sendSchedule(MQTTProtocol.Schedule schedule) { } private void sendDelta(MQTTProtocol.StateValue state) { - sendRequest(new MQTTProtocol.DeltaRequest(state)); + MQTTProtocol.Request request = new MQTTProtocol.DeltaRequest(state); + connection.send(request.getTopic(), gson.toJson(request)); } - private void sendRequest(MQTTProtocol.Request request) { - MqttBrokerConnection conn = connection; - - if (conn != null) { - String json = gson.toJson(request); - logger.trace("Sending {}: {}", request.getTopic(), json); - // 1 here actually corresponds to MQTT qos 0 (AT_MOST_ONCE). Only this value is accepted - // by Roomba, others just cause it to reject the command and drop the connection. - conn.publish(request.getTopic(), json.getBytes(), 1, false); - } - } - - // In order not to mess up our connection state we need to make sure - // that connect() and disconnect() are never running concurrently, so - // they are synchronized - private synchronized void connect() { - logger.debug("Connecting to {}", config.ipaddress); - - try { - InetAddress host = InetAddress.getByName(config.ipaddress); - String blid = this.blid; - - if (blid == null) { - DatagramSocket identSocket = IdentProtocol.sendRequest(host); - DatagramPacket identPacket = IdentProtocol.receiveResponse(identSocket); - IdentProtocol.IdentData ident; - - identSocket.close(); - + private synchronized void getCredentials() { + ThingStatus status = thing.getStatusInfo().getStatus(); + IRobotConfiguration config = getConfigAs(IRobotConfiguration.class); + if (UNINITIALIZED.equals(status) || INITIALIZING.equals(status) || OFFLINE.equals(status)) { + if (UNKNOWN.equals(config.getBlid())) { + @Nullable + String blid = null; try { - ident = IdentProtocol.decodeResponse(identPacket); - } catch (JsonParseException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Malformed IDENT response"); - return; - } - - if (ident.ver < IdentData.MIN_SUPPORTED_VERSION) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Unsupported version " + ident.ver); - return; + blid = LoginRequester.getBlid(config.getIpAddress()); + } catch (IOException exception) { + updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, exception.toString()); } - if (!ident.product.equals(IdentData.PRODUCT_ROOMBA)) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Not a Roomba: " + ident.product); - return; + if (blid != null) { + org.openhab.core.config.core.Configuration configuration = editConfiguration(); + configuration.put(ROBOT_BLID, blid); + updateConfiguration(configuration); } - - blid = ident.blid; - this.blid = blid; } - logger.debug("BLID is: {}", blid); - - if (config.password.isEmpty()) { - RawMQTT mqtt; - + if (UNKNOWN.equals(config.getPassword())) { + @Nullable + String password = null; try { - mqtt = new RawMQTT(host, 8883); - } catch (KeyManagementException | NoSuchAlgorithmException e1) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e1.toString()); + password = LoginRequester.getPassword(config.getIpAddress()); + } catch (KeyManagementException | NoSuchAlgorithmException exception) { + updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, exception.toString()); return; // This is internal system error, no retry + } catch (IOException exception) { + updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, exception.toString()); } - mqtt.requestPassword(); - RawMQTT.Packet response = mqtt.readPacket(); - mqtt.close(); - - if (response != null && response.isValidPasswdPacket()) { - RawMQTT.PasswdPacket passwdPacket = new RawMQTT.PasswdPacket(response); - String password = passwdPacket.getPassword(); - - if (password != null) { - config.password = password; - - Configuration configuration = editConfiguration(); - - configuration.put("password", password); - updateConfiguration(configuration); - - logger.debug("Password successfully retrieved"); - } + if (password != null) { + org.openhab.core.config.core.Configuration configuration = editConfiguration(); + configuration.put(ROBOT_PASSWORD, password.trim()); + updateConfiguration(configuration); } } - - if (config.password.isEmpty()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, - "Authentication on the robot is required"); - scheduleReconnect(); - return; - } - - // BLID is used as both client ID and username. The name of BLID also came from Roomba980-python - MqttBrokerConnection connection = new MqttBrokerConnection(config.ipaddress, RawMQTT.ROOMBA_MQTT_PORT, true, - blid); - - this.connection = connection; - - // Disable sending UNSUBSCRIBE request before disconnecting becuase Roomba doesn't like it. - // It just swallows the request and never sends any response, so stop() method never completes. - connection.setUnsubscribeOnStop(false); - connection.setCredentials(blid, config.password); - connection.setTrustManagers(RawMQTT.getTrustManagers()); - // 1 here actually corresponds to MQTT qos 0 (AT_MOST_ONCE). Only this value is accepted - // by Roomba, others just cause it to reject the command and drop the connection. - connection.setQos(1); - // MQTT connection reconnects itself, so we don't have to call scheduleReconnect() - // when it breaks. Just set the period in ms. - connection.setReconnectStrategy( - new PeriodicReconnectStrategy(RECONNECT_DELAY_SEC * 1000, RECONNECT_DELAY_SEC * 1000)); - connection.start().exceptionally(e -> { - connectionStateChanged(MqttConnectionState.DISCONNECTED, e); - return false; - }).thenAccept(v -> { - if (!v) { - connectionStateChanged(MqttConnectionState.DISCONNECTED, new TimeoutException("Timeout")); - } else { - connectionStateChanged(MqttConnectionState.CONNECTED, null); - } - }); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - scheduleReconnect(); - } - } - - private synchronized void disconnect() { - Future reconnectReq = this.reconnectReq; - MqttBrokerConnection connection = this.connection; - - if (reconnectReq != null) { - reconnectReq.cancel(false); - this.reconnectReq = null; } - if (connection != null) { - connection.stop(); - logger.trace("Closed connection to {}", config.ipaddress); - this.connection = null; + credentialRequester = null; + if (UNKNOWN.equals(config.getBlid()) || UNKNOWN.equals(config.getPassword())) { + credentialRequester = scheduler.schedule(this::getCredentials, 10000, TimeUnit.MILLISECONDS); + } else { + scheduler.execute(this::connect); } } - private void scheduleReconnect() { - reconnectReq = scheduler.schedule(this::connect, RECONNECT_DELAY_SEC, TimeUnit.SECONDS); - } - - public void onConnected() { - updateStatus(ThingStatus.ONLINE); + // In order not to mess up our connection state we need to make sure that connect() + // and disconnect() are never running concurrently, so they are synchronized + private synchronized void connect() { + IRobotConfiguration config = getConfigAs(IRobotConfiguration.class); + final String address = config.getIpAddress(); + logger.debug("Connecting to {}", address); + + final String blid = config.getBlid(); + final String password = config.getPassword(); + if (UNKNOWN.equals(blid) || UNKNOWN.equals(password)) { + final String message = "Robot authentication is required"; + updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message); + scheduler.execute(this::getCredentials); + } else { + final String message = "Robot authentication is successful"; + updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, message); + connection.connect(address, blid, password); + } } - @Override - public void processMessage(String topic, byte[] payload) { - String jsonStr = new String(payload); + public void receive(final String topic, final String json) { MQTTProtocol.StateMessage msg; - logger.trace("Got topic {} data {}", topic, jsonStr); + logger.trace("Got topic {} data {}", topic, json); try { // We are not consuming all the fields, so we have to create the reader explicitly @@ -372,11 +341,11 @@ public void processMessage(String topic, byte[] payload) { // "JSON not fully consumed" exception, because not all the reader's content has been // used up. We want to avoid that also for compatibility reasons because newer iRobot // versions may add fields. - JsonReader jsonReader = new JsonReader(new StringReader(jsonStr)); + JsonReader jsonReader = new JsonReader(new StringReader(json)); msg = gson.fromJson(jsonReader, MQTTProtocol.StateMessage.class); - } catch (JsonParseException e) { - logger.warn("Failed to parse JSON message from {}: {}", config.ipaddress, e.toString()); - logger.warn("Raw contents: {}", payload); + } catch (JsonParseException exception) { + logger.warn("Failed to parse JSON message for {}: {}", thing.getLabel(), exception.toString()); + logger.warn("Raw contents: {}", json); return; } @@ -393,7 +362,7 @@ public void processMessage(String topic, byte[] payload) { String phase = reported.cleanMissionStatus.phase; String command; - if (cycle.equals("none")) { + if ("none".equals(cycle)) { command = CMD_STOP; } else { switch (phase) { @@ -572,47 +541,4 @@ private void reportProperty(String property, @Nullable String value) { updateProperty(property, value); } } - - @Override - public void connectionStateChanged(MqttConnectionState state, @Nullable Throwable error) { - if (state == MqttConnectionState.CONNECTED) { - MqttBrokerConnection connection = this.connection; - - if (connection == null) { - // This would be very strange, but Eclipse forces us to do the check - logger.warn("Established connection without broker pointer"); - return; - } - - updateStatus(ThingStatus.ONLINE); - - // Roomba sends us two topics: - // "wifistat" - reports singnal strength and current robot position - // "$aws/things//shadow/update" - the rest of messages - // Subscribe to everything since we're interested in both - connection.subscribe("#", this).exceptionally(e -> { - logger.warn("MQTT subscription failed: {}", e.getMessage()); - return false; - }).thenAccept(v -> { - if (!v) { - logger.warn("Subscription timeout"); - } else { - logger.trace("Subscription done"); - } - }); - - } else { - String message; - - if (error != null) { - message = error.getMessage(); - logger.warn("MQTT connection failed: {}", message); - } else { - message = null; - logger.warn("MQTT connection failed for unspecified reason"); - } - - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); - } - } } diff --git a/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/utils/LoginRequester.java b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/utils/LoginRequester.java new file mode 100644 index 0000000000000..7e0246f091250 --- /dev/null +++ b/bundles/org.openhab.binding.irobot/src/main/java/org/openhab/binding/irobot/internal/utils/LoginRequester.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010-2021 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.irobot.internal.utils; + +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.MQTT_PORT; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.TRUST_MANAGERS; +import static org.openhab.binding.irobot.internal.IRobotBindingConstants.UDP_PORT; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.net.ssl.SSLContext; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.irobot.internal.discovery.IRobotDiscoveryService; +import org.openhab.binding.irobot.internal.dto.MQTTProtocol.BlidResponse; + +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; + +/** + * Helper functions to get blid and password. Seems pretty much reinventing a bicycle, + * but it looks like HiveMq doesn't provide for sending and receiving custom packets. + * The {@link LoginRequester#getBlid} and {@link IRobotDiscoveryService} are heavily + * related to each other. + * + * @author Pavel Fedin - Initial contribution + * @author Alexander Falkenstern - Fix password fetching + * + */ +@NonNullByDefault +public class LoginRequester { + private static final Gson GSON = new Gson(); + + public static @Nullable String getBlid(final String ip) throws IOException { + DatagramSocket socket = new DatagramSocket(); + socket.setSoTimeout(1000); // One second + socket.setReuseAddress(true); + + final byte[] bRequest = "irobotmcs".getBytes(StandardCharsets.UTF_8); + DatagramPacket request = new DatagramPacket(bRequest, bRequest.length, InetAddress.getByName(ip), UDP_PORT); + socket.send(request); + + byte[] reply = new byte[1024]; + try { + DatagramPacket packet = new DatagramPacket(reply, reply.length); + socket.receive(packet); + reply = Arrays.copyOfRange(packet.getData(), packet.getOffset(), packet.getLength()); + } finally { + socket.close(); + } + + final String json = new String(reply, 0, reply.length, StandardCharsets.UTF_8); + JsonReader jsonReader = new JsonReader(new StringReader(json)); + BlidResponse msg = GSON.fromJson(jsonReader, BlidResponse.class); + + @Nullable + String blid = msg.robotid; + if (((blid == null) || blid.isEmpty()) && ((msg.hostname != null) && !msg.hostname.isEmpty())) { + String[] parts = msg.hostname.split("-"); + if (parts.length == 2) { + blid = parts[1]; + } + } + + return blid; + } + + public static @Nullable String getPassword(final String ip) + throws KeyManagementException, NoSuchAlgorithmException, IOException { + String password = null; + + SSLContext context = SSLContext.getInstance("SSL"); + context.init(null, TRUST_MANAGERS, new java.security.SecureRandom()); + + Socket socket = context.getSocketFactory().createSocket(ip, MQTT_PORT); + socket.setSoTimeout(3000); + + // 1st byte: MQTT reserved message: 0xF0 + // 2nd byte: Data length: 0x05 + // from 3d byte magic packet data: 0xEFCC3B2900 + final byte[] request = { (byte) 0xF0, (byte) 0x05, (byte) 0xEF, (byte) 0xCC, (byte) 0x3B, (byte) 0x29, 0x00 }; + socket.getOutputStream().write(request); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try { + socket.getInputStream().transferTo(buffer); + } catch (IOException exception) { + // Roomba 980 send no properly EOF, so eat the exception + } finally { + socket.close(); + buffer.flush(); + } + + final byte[] reply = buffer.toByteArray(); + if ((reply.length > request.length) && (reply.length == reply[1] + 2)) { // Add 2 bytes, see request doc above + reply[1] = request[1]; // Hack, that we can find request packet in reply + if (Arrays.equals(request, 0, request.length, reply, 0, request.length)) { + password = new String(Arrays.copyOfRange(reply, request.length, reply.length), StandardCharsets.UTF_8); + } + } + + return password; + } +} diff --git a/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/config/thing.xml b/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/config/thing.xml new file mode 100644 index 0000000000000..162d79d6a91a6 --- /dev/null +++ b/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/config/thing.xml @@ -0,0 +1,24 @@ + + + + + + network-address + + Network address of the robot + true + + + + ID of the robot + + + password + + Password of the robot + + + diff --git a/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing.xml similarity index 96% rename from bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing-types.xml rename to bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing.xml index 99972d7fec9e6..20f85b153f613 100644 --- a/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.irobot/src/main/resources/OH-INF/thing/thing.xml @@ -7,6 +7,7 @@ A Roomba vacuum robot + CleaningRobot @@ -53,17 +54,9 @@ - - - - IP Address or host name of your Roomba - network-address - - - - - + mac + diff --git a/bundles/org.openhab.binding.irobot/src/test/java/org/openhab/binding/irobot/internal/handler/RoombaHandlerTest.java b/bundles/org.openhab.binding.irobot/src/test/java/org/openhab/binding/irobot/internal/handler/RoombaHandlerTest.java index afb9b8250b689..395768ff4820c 100644 --- a/bundles/org.openhab.binding.irobot/src/test/java/org/openhab/binding/irobot/internal/handler/RoombaHandlerTest.java +++ b/bundles/org.openhab.binding.irobot/src/test/java/org/openhab/binding/irobot/internal/handler/RoombaHandlerTest.java @@ -12,22 +12,32 @@ */ package org.openhab.binding.irobot.internal.handler; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.io.IOException; import java.lang.reflect.Field; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.irobot.internal.config.IRobotConfiguration; import org.openhab.core.config.core.Configuration; 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.ThingStatusInfo; +import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.internal.ThingImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.spi.LocationAwareLogger; @@ -39,82 +49,80 @@ * @author Florian Binder - Initial contribution */ +@NonNullByDefault @ExtendWith(MockitoExtension.class) -@TestInstance(Lifecycle.PER_CLASS) -class RoombaHandlerTest { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class RoombaHandlerTest { private static final String IP_ADDRESS = ""; private static final String PASSWORD = ""; + @Nullable private RoombaHandler handler; - private @Mock Thing myThing; + // We have to initialize it to avoid compile errors + private @Mock Thing thing = new ThingImpl(new ThingTypeUID("AA:BB"), ""); + @Nullable private ThingHandlerCallback callback; @BeforeEach void setUp() throws Exception { - Logger l = LoggerFactory.getLogger(RoombaHandler.class); - Field f = l.getClass().getDeclaredField("currentLogLevel"); - f.setAccessible(true); - f.set(l, LocationAwareLogger.TRACE_INT); + Logger logger = LoggerFactory.getLogger(RoombaHandler.class); + Field logLevelField = logger.getClass().getDeclaredField("currentLogLevel"); + logLevelField.setAccessible(true); + logLevelField.set(logger, LocationAwareLogger.TRACE_INT); Configuration config = new Configuration(); config.put("ipaddress", RoombaHandlerTest.IP_ADDRESS); config.put("password", RoombaHandlerTest.PASSWORD); - - Mockito.when(myThing.getConfiguration()).thenReturn(config); - Mockito.when(myThing.getUID()).thenReturn(new ThingUID("mocked", "irobot", "uid")); + Mockito.when(thing.getConfiguration()).thenReturn(config); + Mockito.when(thing.getStatusInfo()) + .thenReturn(new ThingStatusInfo(ThingStatus.UNINITIALIZED, ThingStatusDetail.NONE, "mocked")); + Mockito.lenient().when(thing.getUID()).thenReturn(new ThingUID("mocked", "irobot", "uid")); callback = Mockito.mock(ThingHandlerCallback.class); - handler = new RoombaHandler(myThing); + handler = new RoombaHandler(thing); handler.setCallback(callback); } - // @Test - void testInit() throws InterruptedException, IOException { + @Test + void testConfiguration() throws InterruptedException, IOException { handler.initialize(); - Mockito.verify(myThing, Mockito.times(1)).getConfiguration(); - System.in.read(); + IRobotConfiguration config = thing.getConfiguration().as(IRobotConfiguration.class); + assertEquals(config.getIpAddress(), IP_ADDRESS); + assertEquals(config.getPassword(), PASSWORD); + handler.dispose(); } - // @Test - void testCleanRegion() throws IOException, InterruptedException { + @Test + void testCleanRegion() throws InterruptedException, IOException { handler.initialize(); - System.in.read(); - - ChannelUID cmd = new ChannelUID("my:thi:blabla:command"); + ChannelUID cmd = new ChannelUID(thing.getUID(), "command"); handler.handleCommand(cmd, new StringType("cleanRegions:AABBCCDDEEFFGGHH;2,3")); - System.in.read(); handler.dispose(); } - // @Test - void testDock() throws IOException, InterruptedException { + @Test + void testDock() throws InterruptedException, IOException { handler.initialize(); - System.in.read(); - - ChannelUID cmd = new ChannelUID("my:thi:blabla:command"); + ChannelUID cmd = new ChannelUID(thing.getUID(), "command"); handler.handleCommand(cmd, new StringType("dock")); - System.in.read(); handler.dispose(); } - // @Test - void testStop() throws IOException, InterruptedException { + @Test + void testStop() throws InterruptedException, IOException { handler.initialize(); - System.in.read(); - - ChannelUID cmd = new ChannelUID("my:thi:blabla:command"); + ChannelUID cmd = new ChannelUID(thing.getUID(), "command"); handler.handleCommand(cmd, new StringType("stop")); - System.in.read(); handler.dispose(); } } diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationBindingConstants.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationBindingConstants.java index d279b706d1eff..48137200d2a32 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationBindingConstants.java +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationBindingConstants.java @@ -80,6 +80,8 @@ public class ThirdGenerationBindingConstants { public static final String CHANNEL_DEVICE_LOCAL_LIMIT_EVU_ABSOLUTE = "deviceLocalLimitEVUAbsolute"; public static final String CHANNEL_DEVICE_LOCAL_LIMIT_EVU_RELATIV = "deviceLocalLimitEVURelativ"; public static final String CHANNEL_DEVICE_LOCAL_WORKTIME = "deviceLocalWorktime"; + public static final String CHANNEL_DEVICE_LOCAL_AC_COS_PHI = "deviceLocalACCosPhi"; + public static final String CHANNEL_DEVICE_LOCAL_AC_FREQUENCY = "deviceLocalACFrequency"; public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_AMPERAGE = "deviceLocalACPhase1CurrentAmperage"; public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_POWER = "deviceLocalACPhase1CurrentPower"; public static final String CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_VOLTAGE = "deviceLocalACPhase1CurrentVoltage"; diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelDatatypes.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelDatatypes.java index 75b4e023d1beb..4d426db8cf7cc 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelDatatypes.java +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationChannelDatatypes.java @@ -26,5 +26,6 @@ enum ThirdGenerationChannelDatatypes { WATT, AMPERE, AMPERE_HOUR, - VOLT + VOLT, + HERTZ } diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHandler.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHandler.java index b4c16e004f9fc..6104cdd6f50aa 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHandler.java +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationHandler.java @@ -252,6 +252,10 @@ private void updateChannelValue(String channeluid, ThirdGenerationChannelDatatyp updateState(channeluid, new QuantityType<>(value, Units.VOLT)); break; } + case HERTZ: { + updateState(channeluid, new QuantityType<>(value, Units.HERTZ)); + break; + } default: { // unknown datatype logger.debug("{} not known!", dataType); diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationMappingInverterToChannel.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationMappingInverterToChannel.java index 3d61b8a39a467..715a0855f94cc 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationMappingInverterToChannel.java +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/thirdgeneration/ThirdGenerationMappingInverterToChannel.java @@ -69,6 +69,10 @@ class ThirdGenerationMappingInverterToChannel { ThirdGenerationChannelDatatypes.PERCEMTAGE); addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_WORKTIME, "devices:local", "WorkTime", ThirdGenerationChannelDatatypes.SECONDS); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_COS_PHI, "devices:local:ac", "CosPhi", + ThirdGenerationChannelDatatypes.INTEGER); + addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_FREQUENCY, "devices:local:ac", "Frequency", + ThirdGenerationChannelDatatypes.HERTZ); addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_AMPERAGE, "devices:local:ac", "L1_I", ThirdGenerationChannelDatatypes.AMPERE); addInverterChannel(allInvertersList, CHANNEL_DEVICE_LOCAL_AC_PHASE_1_CURRENT_POWER, "devices:local:ac", "L1_P", diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/Channels.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/Channels.xml index 304aa0ebc6644..1ab775cb73b31 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/Channels.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/Channels.xml @@ -70,6 +70,18 @@ Time + + Number:Dimensionless + + AC Power Factor + + + + Number:Frequency + + AC Frequency + + Number:ElectricCurrent diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS100WITHBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS100WITHBATTERY.xml index 7cf7c17ce5440..c5c3e7c2cabde 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS100WITHBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS100WITHBATTERY.xml @@ -17,6 +17,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS100WITHOUTBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS100WITHOUTBATTERY.xml index 8db1d98e5bcba..0a322e48ce6bb 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS100WITHOUTBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS100WITHOUTBATTERY.xml @@ -16,6 +16,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS42WITHBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS42WITHBATTERY.xml index 1f100e851aa74..0f1cea8ad7347 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS42WITHBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS42WITHBATTERY.xml @@ -17,6 +17,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS42WITHOUTBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS42WITHOUTBATTERY.xml index dfcd4d0da62eb..728d51974a78b 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS42WITHOUTBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS42WITHOUTBATTERY.xml @@ -16,6 +16,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS55WITHBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS55WITHBATTERY.xml index 2860434b592bb..964caeb673be7 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS55WITHBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS55WITHBATTERY.xml @@ -17,6 +17,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS55WITHOUTBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS55WITHOUTBATTERY.xml index e9208610bea74..fe52d83a542eb 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS55WITHOUTBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS55WITHOUTBATTERY.xml @@ -16,6 +16,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS70WITHBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS70WITHBATTERY.xml index 4e99f2301611b..0772a5741b74a 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS70WITHBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS70WITHBATTERY.xml @@ -17,6 +17,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS70WITHOUTBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS70WITHOUTBATTERY.xml index 524cb7d3850c2..5b72df295204d 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS70WITHOUTBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS70WITHOUTBATTERY.xml @@ -16,6 +16,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS85WITHBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS85WITHBATTERY.xml index c7050564bc19a..66945893b24a0 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS85WITHBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS85WITHBATTERY.xml @@ -17,6 +17,8 @@ + + diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS85WITHOUTBATTERY.xml b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS85WITHOUTBATTERY.xml index 9df186e33bd28..1139334ac2934 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS85WITHOUTBATTERY.xml +++ b/bundles/org.openhab.binding.kostalinverter/src/main/resources/OH-INF/thing/PLENTICOREPLUS85WITHOUTBATTERY.xml @@ -16,6 +16,8 @@ + + diff --git a/bundles/org.openhab.binding.lcn/README.md b/bundles/org.openhab.binding.lcn/README.md index db9ecdf7fe416..15afd6d6b76f0 100644 --- a/bundles/org.openhab.binding.lcn/README.md +++ b/bundles/org.openhab.binding.lcn/README.md @@ -249,6 +249,10 @@ After the colon, the LCN "hit type" follows: HIT, MAKE or BREAK (German: kurz, l If multiple keys or key tables are programmed in a single "send keys" command, multiple triggers will be executed. +> Notice: Don't test the command with the "Test command" button in LCN-PRO. +This will send a command from LCN-PRO to openHAB, but openHAB expects the module as the sender. +Simply press the physical button at the module for testing. + ### Remote Control To evaluate commands from LCN remote controls (e.g. LCN-RT or LCN-RT16), the module's I-port behavior must be configured as "IR access control" within *LCN-PRO*: diff --git a/bundles/org.openhab.binding.lcn/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.lcn/src/main/resources/OH-INF/config/config.xml index 0e6dfa6c8b431..97782e96683bc 100644 --- a/bundles/org.openhab.binding.lcn/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.lcn/src/main/resources/OH-INF/config/config.xml @@ -43,7 +43,7 @@ - + The module ID, configured in LCN-PRO @@ -58,7 +58,7 @@ The group number, configured in LCN-PRO - + The module ID of any module in the group. The state of this module is used for visualization of the group as representative for all modules. diff --git a/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/evdev4j/EvdevDevice.java b/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/evdev4j/EvdevDevice.java index f2ca21cf968e9..978aee31a9354 100644 --- a/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/evdev4j/EvdevDevice.java +++ b/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/evdev4j/EvdevDevice.java @@ -189,7 +189,7 @@ public void enable(EvdevLibrary.Type type, int code) { public Collection enumerateKeys() { int minKey = 0; - int maxKey = 255 - 1; + int maxKey = lib.event_type_get_max(EvdevLibrary.Type.KEY.intValue()); List result = new ArrayList<>(); for (int i = minKey; i <= maxKey; i++) { if (has(EvdevLibrary.Type.KEY, i)) { @@ -215,6 +215,11 @@ public int getCode() { public String getName() { return lib.event_code_get_name(EvdevLibrary.Type.KEY.intValue(), code); } + + @Override + public String toString() { + return String.valueOf(code); + } } public static class InputEvent { diff --git a/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/evdev4j/jnr/EvdevLibrary.java b/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/evdev4j/jnr/EvdevLibrary.java index 2a26425e37c41..7eca6b89bf1a0 100644 --- a/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/evdev4j/jnr/EvdevLibrary.java +++ b/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/evdev4j/jnr/EvdevLibrary.java @@ -98,6 +98,8 @@ public InputEvent(Runtime runtime) { int enable_event_type(@In Handle handle, int type); + int event_type_get_max(int type); + int disable_event_type(@In Handle handle, int type); boolean has_event_code(@In Handle handle, int type, int code); diff --git a/bundles/org.openhab.binding.linuxinput/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.linuxinput/src/main/resources/OH-INF/thing/thing-types.xml index b27e32f5f0106..5620a0dec4726 100644 --- a/bundles/org.openhab.binding.linuxinput/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.linuxinput/src/main/resources/OH-INF/thing/thing-types.xml @@ -30,7 +30,7 @@ - + Contact diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/LogReaderHandlerFactory.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/LogReaderHandlerFactory.java index 9feae99ff0156..c720fc49acc3d 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/LogReaderHandlerFactory.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/LogReaderHandlerFactory.java @@ -14,10 +14,7 @@ import static org.openhab.binding.logreader.internal.LogReaderBindingConstants.THING_READER; -import java.util.Collections; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -40,8 +37,7 @@ @NonNullByDefault public class LogReaderHandlerFactory extends BaseThingHandlerFactory { - private static final Set SUPPORTED_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_READER).collect(Collectors.toSet())); + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_READER); @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/config/LogReaderConfiguration.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/config/LogReaderConfiguration.java index d781dfc988e5d..6295d1055d25f 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/config/LogReaderConfiguration.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/config/LogReaderConfiguration.java @@ -12,20 +12,24 @@ */ package org.openhab.binding.logreader.internal.config; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * Configuration class for {@link LogReaderBinding} binding. * * @author Pauli Anttila - Initial contribution */ +@NonNullByDefault public class LogReaderConfiguration { - public String filePath; - public int refreshRate; - public String warningPatterns; - public String warningBlacklistingPatterns; - public String errorPatterns; - public String errorBlacklistingPatterns; - public String customPatterns; - public String customBlacklistingPatterns; + public String filePath = "${OPENHAB_LOGDIR}/openhab.log"; + public int refreshRate = 1000; + public String warningPatterns = "WARN+"; + public @Nullable String warningBlacklistingPatterns; + public String errorPatterns = "ERROR+"; + public @Nullable String errorBlacklistingPatterns; + public @Nullable String customPatterns; + public @Nullable String customBlacklistingPatterns; @Override public String toString() { diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/AbstractLogFileReader.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/AbstractLogFileReader.java index daf8bc24bb8fa..1b85f55ed916d 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/AbstractLogFileReader.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/AbstractLogFileReader.java @@ -16,6 +16,7 @@ import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.logreader.internal.filereader.api.FileReaderListener; import org.openhab.binding.logreader.internal.filereader.api.LogFileReader; import org.slf4j.Logger; @@ -26,6 +27,7 @@ * * @author Pauli Anttila - Initial contribution */ +@NonNullByDefault public abstract class AbstractLogFileReader implements LogFileReader { private final Logger logger = LoggerFactory.getLogger(AbstractLogFileReader.class); diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/FileTailer.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/FileTailer.java index 91b375a8148aa..fa24a06d75364 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/FileTailer.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/FileTailer.java @@ -19,6 +19,7 @@ import org.apache.commons.io.input.Tailer; import org.apache.commons.io.input.TailerListener; import org.apache.commons.io.input.TailerListenerAdapter; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.logreader.internal.filereader.api.FileReaderException; import org.openhab.binding.logreader.internal.filereader.api.LogFileReader; @@ -30,17 +31,21 @@ * * @author Pauli Anttila - Initial contribution */ +@NonNullByDefault public class FileTailer extends AbstractLogFileReader implements LogFileReader { - private final Logger logger = LoggerFactory.getLogger(FileTailer.class); - private Tailer tailer; - private ExecutorService executor; + private @Nullable Tailer tailer; + private @Nullable ExecutorService executor; TailerListener logListener = new TailerListenerAdapter() { @Override public void handle(@Nullable String line) { + if (line == null) { + return; + } + sendLineToListeners(line); } @@ -51,6 +56,10 @@ public void fileNotFound() { @Override public void handle(@Nullable Exception e) { + if (e == null) { + return; + } + sendExceptionToListeners(e); } @@ -62,12 +71,13 @@ public void fileRotated() { @Override public void start(String filePath, long refreshRate) throws FileReaderException { - tailer = new Tailer(new File(filePath), logListener, refreshRate, true, false, true); + Tailer localTailer = new Tailer(new File(filePath), logListener, refreshRate, true, false, true); executor = Executors.newSingleThreadExecutor(); try { logger.debug("Start executor"); - executor.execute(tailer); + executor.execute(localTailer); logger.debug("Executor started"); + this.tailer = localTailer; } catch (Exception e) { throw new FileReaderException(e); } diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/FileReaderException.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/FileReaderException.java index 3f54eeb7a3a4a..b58b912e1bb50 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/FileReaderException.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/FileReaderException.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.logreader.internal.filereader.api; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Exception for file reader errors. * * @author Pauli Anttila - Initial contribution */ +@NonNullByDefault public class FileReaderException extends Exception { private static final long serialVersionUID = 1272957002073978608L; diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/FileReaderListener.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/FileReaderListener.java index 963066f2b9c81..7cc7d542f4bbd 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/FileReaderListener.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/FileReaderListener.java @@ -12,11 +12,15 @@ */ package org.openhab.binding.logreader.internal.filereader.api; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * Interface for file reader listeners. * * @author Pauli Anttila - Initial contribution */ +@NonNullByDefault public interface FileReaderListener { /** @@ -35,12 +39,12 @@ public interface FileReaderListener { * * @param line the line. */ - void handle(String line); + void handle(@Nullable String line); /** * This method is called when exception has occurred. * * @param ex the exception. */ - void handle(Exception ex); + void handle(@Nullable Exception ex); } diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/LogFileReader.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/LogFileReader.java index e00338c46f86c..29491c9f8e272 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/LogFileReader.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/filereader/api/LogFileReader.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.logreader.internal.filereader.api; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Interface for log file readers. * * @author Pauli Anttila - Initial contribution */ +@NonNullByDefault public interface LogFileReader { /** diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/handler/LogHandler.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/handler/LogHandler.java index 0b7ede39e743c..d83bffac8fbfc 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/handler/LogHandler.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/handler/LogHandler.java @@ -17,6 +17,8 @@ import java.time.ZonedDateTime; import java.util.regex.PatternSyntaxException; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.logreader.internal.config.LogReaderConfiguration; import org.openhab.binding.logreader.internal.filereader.api.FileReaderListener; import org.openhab.binding.logreader.internal.filereader.api.LogFileReader; @@ -42,16 +44,17 @@ * @author Miika Jukka - Initial contribution * @author Pauli Anttila - Rewrite */ +@NonNullByDefault public class LogHandler extends BaseThingHandler implements FileReaderListener { private final Logger logger = LoggerFactory.getLogger(LogHandler.class); - private LogReaderConfiguration configuration; + private final LogFileReader fileReader; - private LogFileReader fileReader; + private @NonNullByDefault({}) LogReaderConfiguration configuration; - private SearchEngine errorEngine; - private SearchEngine warningEngine; - private SearchEngine customEngine; + private @Nullable SearchEngine errorEngine; + private @Nullable SearchEngine warningEngine; + private @Nullable SearchEngine customEngine; public LogHandler(Thing thing, LogFileReader fileReader) { super(thing); @@ -97,8 +100,9 @@ public void initialize() { try { warningEngine = new SearchEngine(configuration.warningPatterns, configuration.warningBlacklistingPatterns); errorEngine = new SearchEngine(configuration.errorPatterns, configuration.errorBlacklistingPatterns); - customEngine = new SearchEngine(configuration.customPatterns, configuration.customBlacklistingPatterns); - + String customPatterns = configuration.customPatterns; + customEngine = new SearchEngine(customPatterns != null ? customPatterns : "", + configuration.customBlacklistingPatterns); } catch (PatternSyntaxException e) { logger.debug("Illegal search pattern syntax '{}'. ", e.getMessage(), e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage()); @@ -124,13 +128,17 @@ public void dispose() { shutdown(); } - private void updateChannel(ChannelUID channelUID, Command command, SearchEngine matcher) { - if (command instanceof DecimalType) { - matcher.setMatchCount(((DecimalType) command).longValue()); - } else if (command instanceof RefreshType) { - updateState(channelUID.getId(), new DecimalType(matcher.getMatchCount())); + private void updateChannel(ChannelUID channelUID, Command command, @Nullable SearchEngine matcher) { + if (matcher != null) { + if (command instanceof DecimalType) { + matcher.setMatchCount(((DecimalType) command).longValue()); + } else if (command instanceof RefreshType) { + updateState(channelUID.getId(), new DecimalType(matcher.getMatchCount())); + } else { + logger.debug("Unsupported command '{}' received for channel '{}'", command, channelUID); + } } else { - logger.debug("Unsupported command '{}' received for channel '{}'", command, channelUID); + logger.debug("Cannot update channel as SearchEngine is null."); } } @@ -171,7 +179,7 @@ public void fileRotated() { } @Override - public void handle(String line) { + public void handle(@Nullable String line) { if (line == null) { return; } @@ -180,17 +188,17 @@ public void handle(String line) { updateStatus(ThingStatus.ONLINE); } - if (errorEngine.isMatching(line)) { + if (errorEngine != null && errorEngine.isMatching(line)) { updateChannelIfLinked(CHANNEL_ERRORS, new DecimalType(errorEngine.getMatchCount())); updateChannelIfLinked(CHANNEL_LASTERROR, new StringType(line)); triggerChannel(CHANNEL_NEWERROR, line); } - if (warningEngine.isMatching(line)) { + if (warningEngine != null && warningEngine.isMatching(line)) { updateChannelIfLinked(CHANNEL_WARNINGS, new DecimalType(warningEngine.getMatchCount())); updateChannelIfLinked(CHANNEL_LASTWARNING, new StringType(line)); triggerChannel(CHANNEL_NEWWARNING, line); } - if (customEngine.isMatching(line)) { + if (customEngine != null && customEngine.isMatching(line)) { updateChannelIfLinked(CHANNEL_CUSTOMEVENTS, new DecimalType(customEngine.getMatchCount())); updateChannelIfLinked(CHANNEL_LASTCUSTOMEVENT, new StringType(line)); triggerChannel(CHANNEL_NEWCUSTOM, line); @@ -198,7 +206,7 @@ public void handle(String line) { } @Override - public void handle(Exception ex) { + public void handle(@Nullable Exception ex) { final String msg = ex != null ? ex.getMessage() : ""; logger.debug("Error while trying to read log file: {}. ", msg, ex); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, msg); diff --git a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/searchengine/SearchEngine.java b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/searchengine/SearchEngine.java index 2af9a97713e36..df1e2a6d699d5 100644 --- a/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/searchengine/SearchEngine.java +++ b/bundles/org.openhab.binding.logreader/src/main/java/org/openhab/binding/logreader/internal/searchengine/SearchEngine.java @@ -18,6 +18,7 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; /** @@ -25,6 +26,7 @@ * * @author Pauli Anttila - Initial contribution */ +@NonNullByDefault public class SearchEngine { private List matchers; @@ -39,7 +41,7 @@ public class SearchEngine { * @param blacklistingPatterns search patterns to bypass results which have found by the initial search patterns. * */ - public SearchEngine(String patterns, String blacklistingPatterns) throws PatternSyntaxException { + public SearchEngine(String patterns, @Nullable String blacklistingPatterns) throws PatternSyntaxException { matchers = compilePatterns(patterns); blacklistingMatchers = compilePatterns(blacklistingPatterns); } @@ -80,7 +82,6 @@ public void clearMatchCount() { */ private List compilePatterns(@Nullable String patterns) throws PatternSyntaxException { List patternsList = new ArrayList<>(); - if (patterns != null && !patterns.isEmpty()) { String list[] = patterns.split("\\|"); if (list.length > 0) { diff --git a/bundles/org.openhab.binding.logreader/src/main/resources/OH-INF/thing/reader.xml b/bundles/org.openhab.binding.logreader/src/main/resources/OH-INF/thing/reader.xml index bdcfc3534347f..5ebb0bec51506 100644 --- a/bundles/org.openhab.binding.logreader/src/main/resources/OH-INF/thing/reader.xml +++ b/bundles/org.openhab.binding.logreader/src/main/resources/OH-INF/thing/reader.xml @@ -29,34 +29,34 @@ Path to log file. Empty will default to ${OPENHAB_LOGDIR}/openhab.log ${OPENHAB_LOGDIR}/openhab.log - + Refresh rate in milliseconds for reading logs 1000 - + Search patterns separated by | character for error events. Empty will default to ERROR+ ERROR+ - + Search patterns for blacklisting unwanted error events separated by | character. - + Search patterns separated by | character for warning events. Empty will default to WARN+ WARN+ - + Search patterns for blacklisting unwanted warning events separated by | character. - + Search patterns separated by | character for custom events. - + Search patterns for blacklisting unwanted custom events separated by | character. diff --git a/bundles/org.openhab.binding.mielecloud/NOTICE b/bundles/org.openhab.binding.mielecloud/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/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.mielecloud/README.md b/bundles/org.openhab.binding.mielecloud/README.md new file mode 100644 index 0000000000000..9da358d69b6bc --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/README.md @@ -0,0 +1,623 @@ +# Miele Cloud Binding + +This binding integrates [Miele@home](https://www.miele.de/brand/smarthome-42801.htm) appliances via a cloud connection. +A Miele cloud account and a set of developer credentials is required to use the binding. +The latter can be requested from the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx). + +## Supported Things + +Most Miele appliances that directly connect to the cloud via a Wi-Fi module are supported. +Appliances connecting to the XGW3000 gateway via ZigBee are also supported when registered with the cloud account. +However they might be better supported by the [gateway-based Miele binding](https://www.openhab.org/addons/bindings/miele/). +Depending on the age of your appliance the functionality of the binding might be limited. +Appliances from recent generations will support all functionality. + +The following types of appliances are supported: + +| Appliance type | Thing type | +| -------------------------------- | ------------------------ | +| Coffee Machine | `coffee_system` | +| Dishwasher | `dishwasher` | +| Dish Warmer | `dish_warmer` | +| Freezer | `freezer` | +| Fridge | `fridge` | +| Fridge-Freezer Combination | `fridge_freezer` | +| Hob | `hob` | +| Hood | `hood` | +| Microwave Oven | `oven` | +| Oven | `oven` | +| Robotic Vacuum Cleaner | `robotic_vacuum_cleaner` | +| Tumble Dryer | `dryer` | +| Washer Dryer | `washer_dryer` | +| Washing Machine | `washing_machine` | +| Wine Cabinet | `wine_storage` | +| Wine Cabinet Freezer Combination | `wine_storage` | + +## Discovery + +Please take the following steps prior to using the binding. Create a Miele cloud account in the Miele@mobile app for [Android](https://play.google.com/store/apps/details?id=de.miele.infocontrol&hl=en_US) or [iOS](https://apps.apple.com/de/app/miele-mobile/id930406907?l=en) (if not already done). +Afterwards, pair your appliances. +Once your appliances are set up, register at the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx). +You will receive a pair of client ID and client secret which will be used to pair your Miele cloud account to the Miele cloud openHAB binding. +Keep these credentials to yourself and treat them like a password! +It may take some time until the registration e-mail arrives. + +There is no auto discovery for the Miele cloud account. +The account is paired using OAuth2 with your Miele login and the developer credentials obtained from the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx). +To pair the account go to the binding's configuration UI at `https:///mielecloud`. +For a standard openHABian Pi installation the address is [https://openhabianpi:8443/mielecloud](https://openhabianpi:8443/mielecloud). +Note that your browser will file a warning that the certificate is self-signed. +This is fine and you can safely continue. +It is also possible to use an unsecured connection for pairing but it is strongly recommended to use a secured connection because your credentials will otherwise be transferred without encryption over the local network. +For more information on this topic, see [Securing access to openHAB](https://www.openhab.org/docs/installation/security.html#encrypted-communication). +For a detailed walk through the account configuration, see [Account Configuration Example](#account-configuration-example). + +Once a Miele account is paired, all supported appliances are automatically discovered as individual things and placed in the inbox. +They can then be paired with your favorite management UI. +As an alternative, the binding configuration UI provides a things-file template per paired account that can be used to pair the appliances. + +## Thing Configuration + +A Miele cloud account needs to be configured to get access to your appliances. +After that appliances can be configured. + +### Account Configuration + +The Miele cloud account must be paired via the binding configuration UI before a bridge that relies on it can be configured in openHAB. +For details on the configuration UI see [Discovery](#discovery) and [Account Configuration Example](#account-configuration-example). +The account serves as a bridge for the things representing your appliances. +On success the configuration assistant will directly configure the account without requiring further actions. +As an alternative, it provides a things-file template. + +The account has the following parameters: + +| Name | Type | Description | +| ----------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| email | required | E-mail address identifying this account. This exists only to distinguish accounts. If the address is changed after authorization then the account needs to be authorized again. | +| locale | optional | The locale to use for full text channels of things from this account. Possible values are `en`, `de`, `da`, `es`, `fr`, `it`, `nl`, `nb`. Default is `en`. | + + +### Appliance Configuration + +The binding configuration UI will show a things-file template containing things for all supported appliances from the paired account. +This can be used as a starting point for a custom things-file. + +All Miele cloud appliance things have the following parameters: + +| Name | Type | Description | +| ---------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| deviceIdentifier | required | Technical device identifier uniquely identifying the Miele appliance. Use the discovery result or the things-file template to obtain it. | + + +## Channels + +The following table lists all available channels. +See the following chapters for detailed information about which appliance supports which channels. +Depending on the exact appliance configuration not all channels might be supported, e.g. a hob with four plates will only fill the channels for plates 1-4. +Channel ID and channel type ID match unless noted. + +| Channel Type ID | Item Type | Description | Read only | +| ----------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| remote_control_can_be_started | Switch | Indicates if this device can be started remotely. | Yes | +| remote_control_can_be_stopped | Switch | Indicates if this device can be stopped remotely. | Yes | +| remote_control_can_be_paused | Switch | Indicates if this device can be paused remotely. | Yes | +| remote_control_can_be_switched_on | Switch | Indicates if the device can be switched on remotely. | Yes | +| remote_control_can_be_switched_off | Switch | Indicates if the device can be switched off remotely. | Yes | +| remote_control_can_set_program_active | Switch | Indicates if the active program of the device can be set remotely. | Yes | +| spinning_speed | String | The spinning speed of the active program. | Yes | +| spinning_speed_raw | Number | The raw spinning speed of the active program. | Yes | +| program_active | String | The active program of the device. | Yes | +| program_active_raw | Number | The raw active program of the device. | Yes | +| dish_warmer_program_active | String | The active program of the device. | No | +| vacuum_cleaner_program_active | String | The active program of the device. | No | +| program_phase | String | The phase of the active program. | Yes | +| program_phase_raw | Number | The raw phase of the active program. | Yes | +| operation_state | String | The operation state of the device. | Yes | +| operation_state_raw | Number | The raw operation state of the device. | Yes | +| program_start | Switch | Starts the currently selected program. | No | +| program_stop | Switch | Stops the currently selected program. | No | +| program_start_stop | String | Starts or stops the currently selected program. | No | +| program_start_stop_pause | String | Starts, stops or pauses the currently selected program. | No | +| power_state_on_off | String | Switches the device On or Off. | No | +| finish_state | Switch | Indicates whether the most recent program finished. | Yes | +| delayed_start_time | Number | The delayed start time of the selected program. | Yes | +| program_remaining_time | Number | The remaining time of the active program. | Yes | +| program_elapsed_time | Number | The elapsed time of the active program. | Yes | +| program_progress | Number | The progress of the active program. | Yes | +| drying_target | String | The target drying step of the laundry. | Yes | +| drying_target_raw | Number | The raw target drying step of the laundry. | Yes | +| pre_heat_finished | Switch | Indicates whether the pre-heating finished. | Yes | +| temperature_target | Number | The target temperature of the device. | Yes | +| temperature_current | Number | The currently measured temperature of the device. | Yes | +| ventilation_power | String | The current ventilation power of the hood. | Yes | +| ventilation_power_raw | Number | The current raw ventilation power of the hood. | Yes | +| error_state | Switch | Indication flag which signals an error state for the device. | Yes | +| info_state | Switch | Indication flag which signals an information of the device. | Yes | +| fridge_super_cool | Switch | Start the super cooling mode of the fridge. | No | +| freezer_super_freeze | Switch | Start the super freezing mode of the freezer. | No | +| super_cool_can_be_controlled | Switch | Indicates if super cooling can be toggled. | Yes | +| super_freeze_can_be_controlled | Switch | Indicates if super freezing can be toggled | Yes | +| fridge_temperature_target | Number | The target temperature of the fridge. | Yes | +| fridge_temperature_current | Number | The currently measured temperature of the fridge. | Yes | +| freezer_temperature_target | Number | The target temperature of the freezer. | Yes | +| freezer_temperature_current | Number | The currently measured temperature of the freezer. | Yes | +| top_temperature_target | Number | The target temperature of the top area. | Yes | +| top_temperature_current | Number | The currently measured temperature of the top area. | Yes | +| middle_temperature_target | Number | The target temperature of the middle area. | Yes | +| middle_temperature_current | Number | The currently measured temperature of the middle area. | Yes | +| bottom_temperature_target | Number | The target temperature of the bottom area. | Yes | +| bottom_temperature_current | Number | The currently measured temperature of the bottom area. | Yes | +| light_switch | Switch | Indicates if the light of the device is enabled. | No | +| light_can_be_controlled | Switch | Indicates if the light of the device can be controlled. | Yes | +| plate_power_step | String | The power level of the heating plate. | Yes | +| plate_power_step_raw | Number | The raw power level of the heating plate. | Yes | +| door_state | Switch | Indicates if the door of the device is open. | Yes | +| door_alarm | Switch | Indicates if the door alarm of the device is active. | Yes | +| battery_level | Number | The battery level of the robotic vacuum cleaner. | Yes | + +### Coffee System + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- program_active +- program_active_raw +- program_phase +- program_phase_raw +- operation_state +- operation_state_raw +- finish_state +- power_state_on_off +- program_remaining_time +- program_elapsed_time +- error_state +- info_state +- light_switch +- light_can_be_controlled + +### Dish Warmer + +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- dish_warmer_program_active +- program_active_raw +- operation_state +- operation_state_raw +- power_state_on_off +- finish_state +- program_remaining_time +- program_elapsed_time +- program_progress +- temperature_target +- temperature_current +- error_state +- info_state +- door_state + +### Dishwasher + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- program_active +- program_active_raw +- program_phase +- program_phase_raw +- operation_state +- operation_state_raw +- program_start_stop +- finish_state +- power_state_on_off +- delayed_start_time +- program_remaining_time +- program_elapsed_time +- program_progress +- error_state +- info_state +- door_state + +### Tumble Dryer + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- program_active +- program_active_raw +- program_phase +- program_phase_raw +- operation_state +- operation_state_raw +- program_start_stop +- finish_state +- power_state_on_off +- delayed_start_time +- program_remaining_time +- program_elapsed_time +- program_progress +- drying_target +- drying_target_raw +- error_state +- info_state +- light_switch +- light_can_be_controlled +- door_state + +### Freezer + +- operation_state +- operation_state_raw +- error_state +- info_state +- freezer_super_freeze +- super_freeze_can_be_controlled +- freezer_temperature_target +- freezer_temperature_current +- door_state +- door_alarm + +### Fridge + +- operation_state +- operation_state_raw +- error_state +- info_state +- fridge_super_cool +- super_cool_can_be_controlled +- fridge_temperature_target +- fridge_temperature_current +- door_state +- door_alarm + +### Fridge Freezer + +- operation_state +- operation_state_raw +- error_state +- info_state +- fridge_super_cool +- freezer_super_freeze +- super_cool_can_be_controlled +- super_freeze_can_be_controlled +- fridge_temperature_target +- fridge_temperature_current +- freezer_temperature_target +- freezer_temperature_current +- door_state +- door_alarm + +### Hob + +- operation_state +- operation_state_raw +- error_state +- info_state +- plate_1_power_step to plate_6_power_step with channel type ID plate_power_step +- plate_1_power_step_raw to plate_6_power_step_raw with channel type ID plate_power_step_raw + +### Hood + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- program_phase +- program_phase_raw +- operation_state +- operation_state_raw +- power_state_on_off +- ventilation_power +- ventilation_power_raw +- error_state +- info_state +- light_switch +- light_can_be_controlled + +### Oven + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- program_active +- program_active_raw +- program_phase +- program_phase_raw +- operation_state +- operation_state_raw +- program_start_stop +- finish_state +- power_state_on_off +- delayed_start_time +- program_remaining_time +- program_elapsed_time +- program_progress +- pre_heat_finished +- temperature_target +- temperature_current +- error_state +- info_state +- light_switch +- light_can_be_controlled +- door_state + +### Robotic Vacuum Cleaner + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_paused +- remote_control_can_set_program_active +- vacuum_cleaner_program_active +- program_active_raw +- operation_state +- operation_state_raw +- finish_state +- program_start_stop_pause +- power_state_on_off +- error_state +- info_state +- battery_level + +### Washer Dryer + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- spinning_speed +- spinning_speed_raw +- program_active +- program_active_raw +- program_phase +- program_phase_raw +- operation_state +- operation_state_raw +- program_start_stop +- finish_state +- power_state_on_off +- delayed_start_time +- program_remaining_time +- program_elapsed_time +- program_progress +- drying_target +- drying_target_raw +- error_state +- info_state +- temperature_target +- light_switch +- light_can_be_controlled +- door_state + +### Washing Machine + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- spinning_speed +- spinning_speed_raw +- program_active +- program_active_raw +- program_phase +- program_phase_raw +- operation_state +- operation_state_raw +- program_start_stop +- finish_state +- power_state_on_off +- delayed_start_time +- program_remaining_time +- program_elapsed_time +- program_progress +- error_state +- info_state +- temperature_target +- light_switch +- light_can_be_controlled +- door_state + +### Wine Storage + +- remote_control_can_be_started +- remote_control_can_be_stopped +- remote_control_can_be_switched_on +- remote_control_can_be_switched_off +- operation_state +- operation_state_raw +- power_state_on_off +- error_state +- info_state +- temperature_target +- temperature_current +- top_temperature_target +- top_temperature_current +- middle_temperature_target +- middle_temperature_current +- bottom_temperature_target +- bottom_temperature_current + +### Note on plate_power_step channels + +Hob things have an additional property `plateCount` that indicates the number of plates present on the appliance. +Only the channels `plate_1_power_step` to `plate_x_power_step` will be populated by the binding where `x` is the value of the `plateCount` property. + +The plate numbers do not represent the physical layout of the plates on the appliance, but always start with the `plate_1_power_step` channel. +This means that a hob with two plates will have `plate_1_power_step` and `plate_2_power_step` populated and all other `plate_x_power_step` channels empty. + +The `plate_x_power_step` channels show the current power step of the according plate. +**Please note that some hobs may use dynamic numbering for plates.** +Hobs that use dynamic numbering will use the first power step channel that is currently at a power step of zero when the plate is turned on. +Additionally, when a plate is turned off all other plates with higher numbers will decrease their number by one. +For example if plate 1, 2 and 3 are active and plate 1 is turned off then plate 2 will become plate 1, plate 3 will become plate 2 and plate 3 will have a power step of zero. +This behavior is a fixed part of the affected appliances and cannot be changed. + +### Note on door_state channel + +The `door_state` channel might not always provide a value matching the actual state. +For example, a washing machine will not provide a valid `door_state` when the appliance is turned off. +A valid door state can be expected when the appliance is in one of the following raw operation states, compare the `operation_state_raw` channel: + +- `3`: Program selected +- `4`: Program selected, waiting to start +- `5`: Running +- `6`: Paused + +## Properties + +The following chapters list the properties offered by appliances. + +### Common Properties + +| Property Name | Description | +| ------------- | ----------------------------------------------------------------------------- | +| serialNumber | Serial number of the appliance, only present for physical appliances | +| modelId | Model ID of the appliance | +| vendor | Always "Miele" | + +### Account + +| Property Name | Description | +| ------------- | ----------------------------------------------------------------------------- | +| connection | Type of connection used by the account, always "INTERNET" | +| accessToken | The currently used OAuth 2 access token for accessing the Miele 3rd Party API | + +### Hob + +| Property Name | Description | +| ------------- | ----------------------------------------------------------------------------- | +| plateCount | Number of plates offered by the appliance | + +## Full Example + +### demo.things: + +``` +Bridge mielecloud:account:home [ email="me@openhab.org", locale="en" ] { + Thing coffee_system 000703261234 "Coffee machine CVA7440" [ deviceIdentifier="000703261234" ] + Thing hob 000160102345 "Cooktop KM7677" [ deviceIdentifier="000160102345" ] +} +``` + +### demo.items: + +``` +// Coffee system +Switch coffee_system_remote_control_can_be_started { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_started" } +Switch coffee_system_remote_control_can_be_stopped { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_stopped" } +Switch coffee_system_remote_control_can_be_switched_on { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_switched_on" } +Switch coffee_system_remote_control_can_be_switched_off { channel="mielecloud:coffee_system:home:000703261234:remote_control_can_be_switched_off" } +String coffee_system_program_active { channel="mielecloud:coffee_system:home:000703261234:program_active" } +String coffee_system_program_phase { channel="mielecloud:coffee_system:home:000703261234:program_phase" } +String coffee_system_power_state_on_off { channel="mielecloud:coffee_system:home:000703261234:power_state_on_off" } +String coffee_system_operation_state { channel="mielecloud:coffee_system:home:000703261234:operation_state" } +Switch coffee_system_finish_state { channel="mielecloud:coffee_system:home:000703261234:finish_state" } +Number coffee_system_program_remaining_time { channel="mielecloud:coffee_system:home:000703261234:program_remaining_time" } +Switch coffee_system_error_state { channel="mielecloud:coffee_system:home:000703261234:error_state" } +Switch coffee_system_info_state { channel="mielecloud:coffee_system:home:000703261234:info_state" } +Switch coffee_system_light_switch { channel="mielecloud:coffee_system:home:000703261234:light_switch" } +Switch coffee_system_light_can_be_controlled { channel="mielecloud:coffee_system:home:000703261234:light_can_be_controlled" } + +// Hob +Switch hob_remote_control_can_be_started { channel="mielecloud:hob:home:000160102345:remote_control_can_be_started" } +Switch hob_remote_control_can_be_stopped { channel="mielecloud:hob:home:000160102345:remote_control_can_be_stopped" } +String hob_operation_state { channel="mielecloud:hob:home:000160102345:operation_state" } +Switch hob_error_state { channel="mielecloud:hob:home:000160102345:error_state" } +Switch hob_info_state { channel="mielecloud:hob:home:000160102345:info_state" } +Switch hob_plate_1_is_present { channel="mielecloud:hob:home:000160102345:plate_1_is_present" } +String hob_plate_1_power_step { channel="mielecloud:hob:home:000160102345:plate_1_power_step" } +Switch hob_plate_2_is_present { channel="mielecloud:hob:home:000160102345:plate_2_is_present" } +String hob_plate_2_power_step { channel="mielecloud:hob:home:000160102345:plate_2_power_step" } +Switch hob_plate_3_is_present { channel="mielecloud:hob:home:000160102345:plate_3_is_present" } +String hob_plate_3_power_step { channel="mielecloud:hob:home:000160102345:plate_3_power_step" } +Switch hob_plate_4_is_present { channel="mielecloud:hob:home:000160102345:plate_4_is_present" } +String hob_plate_4_power_step { channel="mielecloud:hob:home:000160102345:plate_4_power_step" } +Switch hob_plate_5_is_present { channel="mielecloud:hob:home:000160102345:plate_5_is_present" } +String hob_plate_5_power_step { channel="mielecloud:hob:home:000160102345:plate_5_power_step" } +Switch hob_plate_6_is_present { channel="mielecloud:hob:home:000160102345:plate_6_is_present" } +String hob_plate_6_power_step { channel="mielecloud:hob:home:000160102345:plate_6_power_step" } +``` + +### demo.sitemap: + +``` +sitemap demo label="Kitchen" +{ + Frame { + // Coffee system + Text item=coffee_system_program_active + Text item=coffee_system_program_phase + Text item=coffee_system_power_state_on_off + Text item=coffee_system_operation_state + Switch item=coffee_system_finish_state + Default item=coffee_system_program_remaining_time + Switch item=coffee_system_error_state + Switch item=coffee_system_info_state + Switch item=coffee_system_light_switch + + // Hob + Text item=hob_operation_state + Switch item=hob_error_state + Switch item=hob_info_state + Text item=hob_plate_1_power_step + Text item=hob_plate_2_power_step + Text item=hob_plate_3_power_step + Text item=hob_plate_4_power_step + Text item=hob_plate_5_power_step + Text item=hob_plate_6_power_step + } +} +``` + +## Account Configuration Example + +The configuration UI is accessible at `https:///mielecloud`. +See [Discovery](#discovery) for a detailed description of how to open the configuration UI in a browser. + +When first opening the configuration UI no account will be paired. + +![Empty Account Overview](doc/account-overview-empty.png) + +We strongly recommend to use a secure connection for pairing, details on this topic can also be found in the [Discovery](#discovery) section. +Click `Pair Account` to start the pairing process. +If not already done, go to the [Miele Developer Portal](https://www.miele.com/f/com/en/register_api.aspx), register there and wait for the confirmation e-mail. +Obtain your client ID and client secret according to the instructions presented there. +Once you obtained your client ID and client secret continue pairing by filling in your client ID, client secret, bridge ID and an e-mail address that you wish to use for identifying the account. +You may choose any bridge ID you like as long as you only use letters, numbers, underscores and dashes. +The e-mail address does not need to match the e-mail address used for your Miele Cloud Account. +If you need to change the e-mail address later then you will need to authorize the account again. + +![Pair Account](doc/pair-account.png) + +A click on `Pair Account` will take you to the Miele cloud service login form where you need to log in with the same account as you used for the Miele@mobile app. + +![Miele Login Form](doc/miele-login.png) + +When this is the first time you pair an account, you will need to allow openHAB to access your account. + +When everything worked, you are presented with a page stating that pairing was successful. +Select the locale which should be used to display localized texts in openHAB channels. +From here, you have two options: +Either let the binding automatically configure a bridge instance or copy the presented things-file template to a things-file and return to the overview page. + +![Pairing Successful](doc/pairing-success.png) + +Once the bridge instance is `ONLINE`, you can either pair things for all appliances via your favorite management UI or use a things-file. +The account overview provides a things-file template that is shown when you expand the account. +This can serve as a starting point for your own things-file. + +![Account Overview With Bridge](doc/account-overview-with-bridge.png) + +## Rule Ideas + +Here are some ideas on what could be done with this binding. You have more ideas or even an example? Great! Feel free to contribute! + +- Notify yourself of a finished dishwasher, tumble dryer, washer dryer or washing machine, e.g. by changing the lighting +- Control the supercooler / superfreezer of your freezer, fridge or fridge-freezer combination with a voice assistant +- Notify yourself when the oven has finished pre-heating + +## Acknowledgements + +The development of this binding was initiated and sponsored by Miele & Cie. KG. + diff --git a/bundles/org.openhab.binding.mielecloud/doc/account-overview-empty.png b/bundles/org.openhab.binding.mielecloud/doc/account-overview-empty.png new file mode 100644 index 0000000000000..7b733af0be8bc Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/account-overview-empty.png differ diff --git a/bundles/org.openhab.binding.mielecloud/doc/account-overview-with-bridge.png b/bundles/org.openhab.binding.mielecloud/doc/account-overview-with-bridge.png new file mode 100644 index 0000000000000..2a9361b595f94 Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/account-overview-with-bridge.png differ diff --git a/bundles/org.openhab.binding.mielecloud/doc/miele-login.png b/bundles/org.openhab.binding.mielecloud/doc/miele-login.png new file mode 100644 index 0000000000000..f6a14dbd9999d Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/miele-login.png differ diff --git a/bundles/org.openhab.binding.mielecloud/doc/pair-account.png b/bundles/org.openhab.binding.mielecloud/doc/pair-account.png new file mode 100644 index 0000000000000..54932216e1539 Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/pair-account.png differ diff --git a/bundles/org.openhab.binding.mielecloud/doc/pairing-success.png b/bundles/org.openhab.binding.mielecloud/doc/pairing-success.png new file mode 100644 index 0000000000000..f10bf6284bdc9 Binary files /dev/null and b/bundles/org.openhab.binding.mielecloud/doc/pairing-success.png differ diff --git a/bundles/org.openhab.binding.mielecloud/pom.xml b/bundles/org.openhab.binding.mielecloud/pom.xml new file mode 100644 index 0000000000000..2e6d1f621db9a --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.mielecloud + + openHAB Add-ons :: Bundles :: Miele Cloud Binding + + diff --git a/bundles/org.openhab.binding.mielecloud/src/main/feature/feature.xml b/bundles/org.openhab.binding.mielecloud/src/main/feature/feature.xml new file mode 100644 index 0000000000000..4024caa71eca5 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/feature/feature.xml @@ -0,0 +1,23 @@ + + + + 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.mielecloud/${project.version} + + diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingConstants.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingConstants.java new file mode 100644 index 0000000000000..59b1426ad5a51 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/MieleCloudBindingConstants.java @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link MieleCloudBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Roland Edelhoff - Initial contribution + * @author Björn Lange - Added locale config parameter, added i18n key collection + * @author Benjamin Bolte - Add pre-heat finished and plate step channels, door state and door alarm channels, info + * state channel and map signal flags from API + * @author Björn Lange - Add elapsed time channel, dish warmer thing + */ +@NonNullByDefault +public final class MieleCloudBindingConstants { + + private MieleCloudBindingConstants() { + } + + /** + * ID of the binding. + */ + public static final String BINDING_ID = "mielecloud"; + + /** + * Thing type ID of Miele cloud bridges / accounts. + */ + public static final String BRIDGE_TYPE_ID = "account"; + + /** + * The {@link ThingTypeUID} of Miele cloud bridges / accounts. + */ + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, BRIDGE_TYPE_ID); + + /** + * The {@link ThingTypeUID} of Miele washing machines. + */ + public static final ThingTypeUID THING_TYPE_WASHING_MACHINE = new ThingTypeUID(BINDING_ID, "washing_machine"); + + /** + * The {@link ThingTypeUID} of Miele washer-dryers. + */ + public static final ThingTypeUID THING_TYPE_WASHER_DRYER = new ThingTypeUID(BINDING_ID, "washer_dryer"); + + /** + * The {@link ThingTypeUID} of Miele coffee machines. + */ + public static final ThingTypeUID THING_TYPE_COFFEE_SYSTEM = new ThingTypeUID(BINDING_ID, "coffee_system"); + + /** + * The {@link ThingTypeUID} of Miele fridge-freezers. + */ + public static final ThingTypeUID THING_TYPE_FRIDGE_FREEZER = new ThingTypeUID(BINDING_ID, "fridge_freezer"); + + /** + * The {@link ThingTypeUID} of Miele fridges. + */ + public static final ThingTypeUID THING_TYPE_FRIDGE = new ThingTypeUID(BINDING_ID, "fridge"); + + /** + * The {@link ThingTypeUID} of Miele freezers. + */ + public static final ThingTypeUID THING_TYPE_FREEZER = new ThingTypeUID(BINDING_ID, "freezer"); + + /** + * The {@link ThingTypeUID} of Miele ovens. + */ + public static final ThingTypeUID THING_TYPE_OVEN = new ThingTypeUID(BINDING_ID, "oven"); + + /** + * The {@link ThingTypeUID} of Miele hobs. + */ + public static final ThingTypeUID THING_TYPE_HOB = new ThingTypeUID(BINDING_ID, "hob"); + + /** + * The {@link ThingTypeUID} of Miele wine storages. + */ + public static final ThingTypeUID THING_TYPE_WINE_STORAGE = new ThingTypeUID(BINDING_ID, "wine_storage"); + + /** + * The {@link ThingTypeUID} of Miele dishwashers. + */ + public static final ThingTypeUID THING_TYPE_DISHWASHER = new ThingTypeUID(BINDING_ID, "dishwasher"); + + /** + * The {@link ThingTypeUID} of Miele dryers. + */ + public static final ThingTypeUID THING_TYPE_DRYER = new ThingTypeUID(BINDING_ID, "dryer"); + + /** + * The {@link ThingTypeUID} of Miele hoods. + */ + public static final ThingTypeUID THING_TYPE_HOOD = new ThingTypeUID(BINDING_ID, "hood"); + + /** + * The {@link ThingTypeUID} of Miele dish warmers. + */ + public static final ThingTypeUID THING_TYPE_DISH_WARMER = new ThingTypeUID(BINDING_ID, "dish_warmer"); + + /** + * The {@link ThingTypeUID} of Miele robotic vacuum cleaners. + */ + public static final ThingTypeUID THING_TYPE_ROBOTIC_VACUUM_CLEANER = new ThingTypeUID(BINDING_ID, + "robotic_vacuum_cleaner"); + + /** + * Name of the property storing the OAuth2 access token. + */ + public static final String PROPERTY_ACCESS_TOKEN = "accessToken"; + + /** + * Name of the configuration parameter for the e-mail address. + */ + public static final String CONFIG_PARAM_EMAIL = "email"; + + /** + * Name of the configuration parameter for the device identifier uniquely identifying a Miele device. + */ + public static final String CONFIG_PARAM_DEVICE_IDENTIFIER = "deviceIdentifier"; + + /** + * Name of the configuration parameter for the locale. The locale is stored as a 2-letter language code. + */ + public static final String CONFIG_PARAM_LOCALE = "locale"; + + /** + * Name of the property storing the number of plates for hobs. + */ + public static final String PROPERTY_PLATE_COUNT = "plateCount"; + + /** + * Constants for all channels. + */ + public static final class Channels { + private Channels() { + } + + public static final String REMOTE_CONTROL_CAN_BE_STARTED = "remote_control_can_be_started"; + public static final String REMOTE_CONTROL_CAN_BE_STOPPED = "remote_control_can_be_stopped"; + public static final String REMOTE_CONTROL_CAN_BE_PAUSED = "remote_control_can_be_paused"; + public static final String REMOTE_CONTROL_CAN_BE_SWITCHED_ON = "remote_control_can_be_switched_on"; + public static final String REMOTE_CONTROL_CAN_BE_SWITCHED_OFF = "remote_control_can_be_switched_off"; + public static final String REMOTE_CONTROL_CAN_SET_PROGRAM_ACTIVE = "remote_control_can_set_program_active"; + public static final String SPINNING_SPEED = "spinning_speed"; + public static final String SPINNING_SPEED_RAW = "spinning_speed_raw"; + public static final String PROGRAM_ACTIVE = "program_active"; + public static final String PROGRAM_ACTIVE_RAW = "program_active_raw"; + public static final String DISH_WARMER_PROGRAM_ACTIVE = "dish_warmer_program_active"; + public static final String VACUUM_CLEANER_PROGRAM_ACTIVE = "vacuum_cleaner_program_active"; + public static final String PROGRAM_PHASE = "program_phase"; + public static final String PROGRAM_PHASE_RAW = "program_phase_raw"; + public static final String OPERATION_STATE = "operation_state"; + public static final String OPERATION_STATE_RAW = "operation_state_raw"; + public static final String PROGRAM_START_STOP = "program_start_stop"; + public static final String PROGRAM_START_STOP_PAUSE = "program_start_stop_pause"; + public static final String POWER_ON_OFF = "power_state_on_off"; + public static final String FINISH_STATE = "finish_state"; + public static final String DELAYED_START_TIME = "delayed_start_time"; + public static final String PROGRAM_REMAINING_TIME = "program_remaining_time"; + public static final String PROGRAM_ELAPSED_TIME = "program_elapsed_time"; + public static final String PROGRAM_PROGRESS = "program_progress"; + public static final String DRYING_TARGET = "drying_target"; + public static final String DRYING_TARGET_RAW = "drying_target_raw"; + public static final String PRE_HEAT_FINISHED = "pre_heat_finished"; + public static final String TEMPERATURE_TARGET = "temperature_target"; + public static final String TEMPERATURE_CURRENT = "temperature_current"; + public static final String TEMPERATURE_CORE_TARGET = "temperature_core_target"; + public static final String TEMPERATURE_CORE_CURRENT = "temperature_core_current"; + public static final String VENTILATION_POWER = "ventilation_power"; + public static final String VENTILATION_POWER_RAW = "ventilation_power_raw"; + public static final String ERROR_STATE = "error_state"; + public static final String INFO_STATE = "info_state"; + public static final String FRIDGE_SUPER_COOL = "fridge_super_cool"; + public static final String FREEZER_SUPER_FREEZE = "freezer_super_freeze"; + public static final String SUPER_COOL_CAN_BE_CONTROLLED = "super_cool_can_be_controlled"; + public static final String SUPER_FREEZE_CAN_BE_CONTROLLED = "super_freeze_can_be_controlled"; + public static final String FRIDGE_TEMPERATURE_TARGET = "fridge_temperature_target"; + public static final String FRIDGE_TEMPERATURE_CURRENT = "fridge_temperature_current"; + public static final String FREEZER_TEMPERATURE_TARGET = "freezer_temperature_target"; + public static final String FREEZER_TEMPERATURE_CURRENT = "freezer_temperature_current"; + public static final String TOP_TEMPERATURE_TARGET = "top_temperature_target"; + public static final String TOP_TEMPERATURE_CURRENT = "top_temperature_current"; + public static final String MIDDLE_TEMPERATURE_TARGET = "middle_temperature_target"; + public static final String MIDDLE_TEMPERATURE_CURRENT = "middle_temperature_current"; + public static final String BOTTOM_TEMPERATURE_TARGET = "bottom_temperature_target"; + public static final String BOTTOM_TEMPERATURE_CURRENT = "bottom_temperature_current"; + public static final String LIGHT_SWITCH = "light_switch"; + public static final String LIGHT_CAN_BE_CONTROLLED = "light_can_be_controlled"; + public static final String PLATE_1_POWER_STEP = "plate_1_power_step"; + public static final String PLATE_1_POWER_STEP_RAW = "plate_1_power_step_raw"; + public static final String PLATE_2_POWER_STEP = "plate_2_power_step"; + public static final String PLATE_2_POWER_STEP_RAW = "plate_2_power_step_raw"; + public static final String PLATE_3_POWER_STEP = "plate_3_power_step"; + public static final String PLATE_3_POWER_STEP_RAW = "plate_3_power_step_raw"; + public static final String PLATE_4_POWER_STEP = "plate_4_power_step"; + public static final String PLATE_4_POWER_STEP_RAW = "plate_4_power_step_raw"; + public static final String PLATE_5_POWER_STEP = "plate_5_power_step"; + public static final String PLATE_5_POWER_STEP_RAW = "plate_5_power_step_raw"; + public static final String PLATE_6_POWER_STEP = "plate_6_power_step"; + public static final String PLATE_6_POWER_STEP_RAW = "plate_6_power_step_raw"; + public static final String DOOR_STATE = "door_state"; + public static final String DOOR_ALARM = "door_alarm"; + public static final String BATTERY_LEVEL = "battery_level"; + } + + /** + * Constants for i18n keys. + */ + public static final class I18NKeys { + private I18NKeys() { + } + + public static final String BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_NOT_CONFIGURED = "@text/mielecloud.bridge.status.access.token.not.configured"; + public static final String BRIDGE_STATUS_DESCRIPTION_ACCOUNT_NOT_AUTHORIZED = "@text/mielecloud.bridge.status.account.not.authorized"; + public static final String BRIDGE_STATUS_DESCRIPTION_ACCESS_TOKEN_REFRESH_FAILED = "@text/mielecloud.bridge.status.access.token.refresh.failed"; + public static final String BRIDGE_STATUS_DESCRIPTION_INVALID_EMAIL = "@text/mielecloud.bridge.status.invalid.email"; + public static final String BRIDGE_STATUS_DESCRIPTION_TRANSIENT_HTTP_ERROR = "@text/mielecloud.bridge.status.transient.http.error"; + + public static final String THING_STATUS_DESCRIPTION_WEBSERVICE_MISSING = "@text/mielecloud.thing.status.webservice.missing"; + public static final String THING_STATUS_DESCRIPTION_REMOVED = "@text/mielecloud.thing.status.removed"; + public static final String THING_STATUS_DESCRIPTION_RATELIMIT = "@text/mielecloud.thing.status.ratelimit"; + public static final String THING_STATUS_DESCRIPTION_DISCONNECTED = "@text/mielecloud.thing.status.disconnected"; + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthException.java new file mode 100644 index 0000000000000..e607db6d2324b --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthException.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.auth; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Indicates an error in the OAuth2 authorization process. + * + * @author Roland Edelhoff - Initial contribution + */ +@NonNullByDefault +public class OAuthException extends RuntimeException { + private static final long serialVersionUID = -1863609233382694104L; + + public OAuthException(final String message) { + super(message); + } + + public OAuthException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefreshListener.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefreshListener.java new file mode 100644 index 0000000000000..94b723bdbe955 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefreshListener.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.auth; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Listener that is invoked when an OAuth 2 access token was refreshed. + * + * @author Björn Lange - Initial contribution + */ +@NonNullByDefault +public interface OAuthTokenRefreshListener { + /** + * Invoked when a new access token becomes available. + * + * @param accessToken The new access token. + */ + public void onNewAccessToken(String accessToken); +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefresher.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefresher.java new file mode 100644 index 0000000000000..c5cbd3f35f46b --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OAuthTokenRefresher.java @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.auth; + +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * An {@link OAuthTokenRefresher} offers convenient access to OAuth 2 authentication related functionality, + * especially refreshing the access token. + * + * @author Roland Edelhoff - Initial contribution + * @author Björn Lange - Allow removing tokens from the storage + */ +@NonNullByDefault +public interface OAuthTokenRefresher { + /** + * Sets the listener that is called when the access token was refreshed. + * + * @param listener The listener to register. + * @param serviceHandle The service handle identifying the internal OAuth configuration. + * @throws OAuthException if the listener needs to be registered at an underlying service which is not available + * because the account has not yet been authorized + */ + public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle); + + /** + * Unsets a listener. + * + * @param serviceHandle The service handle identifying the internal OAuth configuration. + */ + public void unsetRefreshListener(String serviceHandle); + + /** + * Refreshes the access and refresh tokens for the given service handle. If an {@link OAuthTokenRefreshListener} is + * registered for the service handle then it is notified after the refresh has completed. + * + * This call will succeed if the access token is still valid or a valid refresh token exists, which can be used to + * refresh the expired access token. If refreshing fails, an {@link OAuthException} is thrown. + * + * @param serviceHandle The service handle identifying the internal OAuth configuration. + * @throws OAuthException if the token cannot be obtained or refreshed + */ + public void refreshToken(String serviceHandle); + + /** + * Gets the currently stored access token from persistent storage. + * + * @param serviceHandle The service handle identifying the internal OAuth configuration. + * @return The currently stored access token or an empty {@link Optional} if there is no stored token. + */ + public Optional getAccessTokenFromStorage(String serviceHandle); + + /** + * Removes the tokens from persistent storage. + * + * Note: Calling this method will force the user to run through the pairing process again in order to obtain a + * working bridge. + * + * @param serviceHandle The service handle identifying the internal OAuth configuration. + */ + public void removeTokensFromStorage(String serviceHandle); +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresher.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresher.java new file mode 100644 index 0000000000000..a947da391fac7 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/auth/OpenHabOAuthTokenRefresher.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.auth; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener; +import org.openhab.core.auth.client.oauth2.AccessTokenResponse; +import org.openhab.core.auth.client.oauth2.OAuthClientService; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.auth.client.oauth2.OAuthResponseException; +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; + +/** + * Handles refreshing of OAuth2 tokens managed by the openHAB runtime. + * + * @author Björn Lange - Initial contribution + */ +@Component +@NonNullByDefault +public final class OpenHabOAuthTokenRefresher implements OAuthTokenRefresher { + private final Logger logger = LoggerFactory.getLogger(OpenHabOAuthTokenRefresher.class); + + private final OAuthFactory oauthFactory; + private Map listenerByServiceHandle = new HashMap<>(); + + @Activate + public OpenHabOAuthTokenRefresher(@Reference OAuthFactory oauthFactory) { + this.oauthFactory = oauthFactory; + } + + @Override + public void setRefreshListener(OAuthTokenRefreshListener listener, String serviceHandle) { + final AccessTokenRefreshListener refreshListener = tokenResponse -> { + final String accessToken = tokenResponse.getAccessToken(); + if (accessToken == null) { + // Fail without exception to ensure that the OAuthClientService notifies all listeners. + logger.warn("Ignoring access token response without access token."); + } else { + listener.onNewAccessToken(accessToken); + } + }; + + OAuthClientService clientService = getOAuthClientService(serviceHandle); + clientService.addAccessTokenRefreshListener(refreshListener); + listenerByServiceHandle.put(serviceHandle, refreshListener); + } + + @Override + public void unsetRefreshListener(String serviceHandle) { + final AccessTokenRefreshListener refreshListener = listenerByServiceHandle.get(serviceHandle); + if (refreshListener != null) { + try { + OAuthClientService clientService = getOAuthClientService(serviceHandle); + clientService.removeAccessTokenRefreshListener(refreshListener); + } catch (OAuthException e) { + logger.warn("Failed to remove refresh listener: OAuth client service is unavailable. Cause: {}", + e.getMessage()); + } + } + listenerByServiceHandle.remove(serviceHandle); + } + + @Override + public void refreshToken(String serviceHandle) { + if (listenerByServiceHandle.get(serviceHandle) == null) { + logger.warn("Token refreshing was requested but there is no token refresh listener registered!"); + return; + } + + OAuthClientService clientService = getOAuthClientService(serviceHandle); + refreshAccessToken(clientService); + } + + private OAuthClientService getOAuthClientService(String serviceHandle) { + final OAuthClientService clientService = oauthFactory.getOAuthClientService(serviceHandle); + if (clientService == null) { + throw new OAuthException("OAuth client service is not available."); + } + return clientService; + } + + private void refreshAccessToken(OAuthClientService clientService) { + try { + final AccessTokenResponse accessTokenResponse = clientService.refreshToken(); + final String accessToken = accessTokenResponse.getAccessToken(); + if (accessToken == null) { + throw new OAuthException("Access token is not available."); + } + } catch (org.openhab.core.auth.client.oauth2.OAuthException e) { + throw new OAuthException("An error occured during token refresh: " + e.getMessage(), e); + } catch (IOException e) { + throw new OAuthException("A network error occured during token refresh: " + e.getMessage(), e); + } catch (OAuthResponseException e) { + throw new OAuthException("Miele cloud service returned an illegal response: " + e.getMessage(), e); + } + } + + @Override + public Optional getAccessTokenFromStorage(String serviceHandle) { + try { + AccessTokenResponse tokenResponse = getOAuthClientService(serviceHandle).getAccessTokenResponse(); + if (tokenResponse == null) { + return Optional.empty(); + } else { + return Optional.of(tokenResponse.getAccessToken()); + } + } catch (OAuthException | org.openhab.core.auth.client.oauth2.OAuthException | IOException + | OAuthResponseException e) { + logger.debug("Cannot obtain access token from persistent storage.", e); + return Optional.empty(); + } + } + + @Override + public void removeTokensFromStorage(String serviceHandle) { + oauthFactory.deleteServiceAndAccessToken(serviceHandle); + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/MieleCloudConfigService.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/MieleCloudConfigService.java new file mode 100644 index 0000000000000..a2b15cc093fd9 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/MieleCloudConfigService.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config; + +import java.util.Hashtable; +import java.util.Map; + +import javax.servlet.ServletException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mielecloud.internal.config.servlet.AccountOverviewServlet; +import org.openhab.binding.mielecloud.internal.config.servlet.CreateBridgeServlet; +import org.openhab.binding.mielecloud.internal.config.servlet.FailureServlet; +import org.openhab.binding.mielecloud.internal.config.servlet.ForwardToLoginServlet; +import org.openhab.binding.mielecloud.internal.config.servlet.PairAccountServlet; +import org.openhab.binding.mielecloud.internal.config.servlet.ResourceLoader; +import org.openhab.binding.mielecloud.internal.config.servlet.ResultServlet; +import org.openhab.binding.mielecloud.internal.config.servlet.SuccessServlet; +import org.openhab.binding.mielecloud.internal.webservice.language.CombiningLanguageProvider; +import org.openhab.binding.mielecloud.internal.webservice.language.JvmLanguageProvider; +import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider; +import org.openhab.binding.mielecloud.internal.webservice.language.OpenHabLanguageProvider; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.config.discovery.inbox.Inbox; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.thing.ThingRegistry; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.http.HttpContext; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles the lifecycle of the Miele Cloud binding's configuration UI. + * + * @author Björn Lange - Initial Contribution + */ +@Component(service = MieleCloudConfigService.class, immediate = true, configurationPid = "binding.mielecloud.configService") +@NonNullByDefault +public final class MieleCloudConfigService { + private static final String ROOT_ALIAS = "/mielecloud"; + private static final String PAIR_ALIAS = ROOT_ALIAS + "/pair"; + private static final String FORWARD_TO_LOGIN_ALIAS = ROOT_ALIAS + "/forwardToLogin"; + private static final String RESULT_ALIAS = ROOT_ALIAS + "/result"; + private static final String SUCCESS_ALIAS = ROOT_ALIAS + "/success"; + private static final String CREATE_BRIDGE_THING_ALIAS = ROOT_ALIAS + "/createBridgeThing"; + private static final String FAILURE_ALIAS = ROOT_ALIAS + "/failure"; + private static final String CSS_ALIAS = ROOT_ALIAS + "/assets/css"; + private static final String JS_ALIAS = ROOT_ALIAS + "/assets/js"; + private static final String IMG_ALIAS = ROOT_ALIAS + "/assets/img"; + + private static final String WEBSITE_RESOURCE_BASE_PATH = "org/openhab/binding/mielecloud/internal/config"; + private static final String WEBSITE_CSS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/css"; + private static final String WEBSITE_JS_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/js"; + private static final String WEBSITE_IMG_RESOURCE_PATH = WEBSITE_RESOURCE_BASE_PATH + "/assets/img"; + + private final Logger logger = LoggerFactory.getLogger(MieleCloudConfigService.class); + + private HttpService httpService; + private OAuthFactory oauthFactory; + private Inbox inbox; + private ThingRegistry thingRegistry; + private LocaleProvider localeProvider; + + /** + * For integration test purposes only. + */ + @Nullable + private AccountOverviewServlet accountOverviewServlet; + + /** + * For integration test purposes only. + */ + @Nullable + private ForwardToLoginServlet forwardToLoginServlet; + + /** + * For integration test purposes only. + */ + @Nullable + private ResultServlet resultServlet; + + /** + * For integration test purposes only. + */ + @Nullable + private SuccessServlet successServlet; + + /** + * For integration test purposes only. + */ + @Nullable + private CreateBridgeServlet createBridgeServlet; + + @Activate + public MieleCloudConfigService(@Reference HttpService httpService, @Reference OAuthFactory oauthFactory, + @Reference Inbox inbox, @Reference ThingRegistry thingRegistry, @Reference LocaleProvider localeProvider) { + this.httpService = httpService; + this.oauthFactory = oauthFactory; + this.inbox = inbox; + this.thingRegistry = thingRegistry; + this.localeProvider = localeProvider; + } + + @Nullable + public AccountOverviewServlet getAccountOverviewServlet() { + return accountOverviewServlet; + } + + @Nullable + public ForwardToLoginServlet getForwardToLoginServlet() { + return forwardToLoginServlet; + } + + @Nullable + public ResultServlet getResultServlet() { + return resultServlet; + } + + @Nullable + public SuccessServlet getSuccessServlet() { + return successServlet; + } + + @Nullable + public CreateBridgeServlet getCreateBridgeServlet() { + return createBridgeServlet; + } + + @Activate + protected void activate(ComponentContext componentContext, Map properties) { + registerWebsite(componentContext.getBundleContext()); + } + + private void registerWebsite(BundleContext bundleContext) { + ResourceLoader resourceLoader = new ResourceLoader(WEBSITE_RESOURCE_BASE_PATH, bundleContext); + OAuthAuthorizationHandler authorizationHandler = new OAuthAuthorizationHandlerImpl(oauthFactory, + ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON)); + + try { + HttpContext httpContext = httpService.createDefaultHttpContext(); + httpService.registerServlet(ROOT_ALIAS, + accountOverviewServlet = new AccountOverviewServlet(resourceLoader, thingRegistry, inbox), + new Hashtable<>(), httpContext); + httpService.registerServlet(PAIR_ALIAS, new PairAccountServlet(resourceLoader), new Hashtable<>(), + httpContext); + httpService.registerServlet(FORWARD_TO_LOGIN_ALIAS, + forwardToLoginServlet = new ForwardToLoginServlet(authorizationHandler), new Hashtable<>(), + httpContext); + httpService.registerServlet(RESULT_ALIAS, resultServlet = new ResultServlet(authorizationHandler), + new Hashtable<>(), httpContext); + httpService.registerServlet(SUCCESS_ALIAS, + successServlet = new SuccessServlet(resourceLoader, createLanguageProvider()), new Hashtable<>(), + httpContext); + httpService.registerServlet(CREATE_BRIDGE_THING_ALIAS, + createBridgeServlet = new CreateBridgeServlet(inbox, thingRegistry), new Hashtable<>(), + httpContext); + httpService.registerServlet(FAILURE_ALIAS, new FailureServlet(resourceLoader), new Hashtable<>(), + httpContext); + httpService.registerResources(CSS_ALIAS, WEBSITE_CSS_RESOURCE_PATH, httpContext); + httpService.registerResources(JS_ALIAS, WEBSITE_JS_RESOURCE_PATH, httpContext); + httpService.registerResources(IMG_ALIAS, WEBSITE_IMG_RESOURCE_PATH, httpContext); + logger.debug("Registered Miele Cloud binding website at /mielecloud"); + } catch (NamespaceException | ServletException e) { + logger.warn( + "Failed to register Miele Cloud binding website. Miele Cloud binding website will not be available.", + e); + unregisterWebsite(); + } + } + + private LanguageProvider createLanguageProvider() { + return new CombiningLanguageProvider(new OpenHabLanguageProvider(localeProvider), new JvmLanguageProvider()); + } + + @Deactivate + protected void deactivate() { + unregisterWebsite(); + } + + private void unregisterWebsite() { + unregisterWebResource(ROOT_ALIAS); + unregisterWebResource(PAIR_ALIAS); + unregisterWebResource(FORWARD_TO_LOGIN_ALIAS); + unregisterWebResource(RESULT_ALIAS); + unregisterWebResource(SUCCESS_ALIAS); + unregisterWebResource(CREATE_BRIDGE_THING_ALIAS); + unregisterWebResource(CSS_ALIAS); + unregisterWebResource(JS_ALIAS); + unregisterWebResource(IMG_ALIAS); + forwardToLoginServlet = null; + resultServlet = null; + createBridgeServlet = null; + logger.debug("Unregistered Miele Cloud binding website at /mielecloud"); + } + + private void unregisterWebResource(String alias) { + try { + httpService.unregister(alias); + } catch (IllegalArgumentException e) { + logger.warn("Failed to unregister Miele Cloud binding website alias {}", alias, e); + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandler.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandler.java new file mode 100644 index 0000000000000..9acf5030a715f --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandler.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException; +import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException; +import org.openhab.core.thing.ThingUID; + +/** + * Handles OAuth 2 authorization processes. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public interface OAuthAuthorizationHandler { + /** + * Begins the authorization process after the user provided client ID, client secret and a bridge ID. + * + * @param clientId Client ID. + * @param clientSecret Client secret. + * @param bridgeUid The UID of the bridge to authorize. + * @param email E-mail address identifying the account to authorize. + * @throws OngoingAuthorizationException if there already is an ongoing authorization. + */ + void beginAuthorization(String clientId, String clientSecret, ThingUID bridgeUid, String email); + + /** + * Creates the authorization URL for the ongoing authorization. + * + * @param redirectUri The URI to which the user is redirected after a successful login. This should point to our own + * service. + * @return The authorization URL to which the user is redirected for the log in. + * @throws NoOngoingAuthorizationException if there is no ongoing authorization. + * @throws OAuthException if the authorization URL cannot be determined. In this case the ongoing authorization is + * cancelled. + */ + String getAuthorizationUrl(String redirectUri); + + /** + * Gets the UID of the bridge that is currently being authorized. + */ + ThingUID getBridgeUid(); + + /** + * Gets the e-mail address associated with the account that is currently being authorized. + */ + String getEmail(); + + /** + * Completes the authorization by extracting the authorization code from the given redirection URL, fetching the + * access token response and persisting it. After this method succeeded the access token can be read from the + * persistent storage. + * + * @param redirectUrlWithParameters The URL the remote service redirected the user to. This is the URL our servlet + * was called with. + * @throws NoOngoingAuthorizationException if there is no ongoing authorization. + * @throws OAuthException if the authorization failed. In this case the ongoing authorization is cancelled. + */ + void completeAuthorization(String redirectUrlWithParameters); + + /** + * Gets the access token from persistent storage. + * + * @param email E-mail address for which the access token is requested. + * @return The access token. + * @throws OAuthException if the access token cannot be obtained. + */ + String getAccessToken(String email); +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImpl.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImpl.java new file mode 100644 index 0000000000000..86ef742af1e66 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/OAuthAuthorizationHandlerImpl.java @@ -0,0 +1,213 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mielecloud.internal.auth.OAuthException; +import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException; +import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException; +import org.openhab.binding.mielecloud.internal.webservice.DefaultMieleWebservice; +import org.openhab.core.auth.client.oauth2.AccessTokenResponse; +import org.openhab.core.auth.client.oauth2.OAuthClientService; +import org.openhab.core.auth.client.oauth2.OAuthFactory; +import org.openhab.core.auth.client.oauth2.OAuthResponseException; +import org.openhab.core.thing.ThingUID; + +/** + * {@link OAuthAuthorizationHandler} implementation handling the OAuth 2 authorization via openHAB services. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class OAuthAuthorizationHandlerImpl implements OAuthAuthorizationHandler { + private static final String TOKEN_URL = DefaultMieleWebservice.THIRD_PARTY_ENDPOINTS_BASENAME + "/token"; + private static final String AUTHORIZATION_URL = DefaultMieleWebservice.THIRD_PARTY_ENDPOINTS_BASENAME + "/login"; + + private static final long AUTHORIZATION_TIMEOUT_IN_MINUTES = 5; + + private final OAuthFactory oauthFactory; + private final ScheduledExecutorService scheduler; + + @Nullable + private OAuthClientService oauthClientService; + @Nullable + private ThingUID bridgeUid; + @Nullable + private String email; + @Nullable + private String redirectUri; + @Nullable + private ScheduledFuture timer; + @Nullable + private LocalDateTime timerExpiryTimestamp; + + /** + * Creates a new {@link OAuthAuthorizationHandlerImpl}. + * + * @param oauthFactory Factory for accessing the {@link OAuthClientService}. + * @param scheduler System-wide scheduler. + */ + public OAuthAuthorizationHandlerImpl(OAuthFactory oauthFactory, ScheduledExecutorService scheduler) { + this.oauthFactory = oauthFactory; + this.scheduler = scheduler; + } + + @Override + public synchronized void beginAuthorization(String clientId, String clientSecret, ThingUID bridgeUid, + String email) { + if (this.oauthClientService != null) { + throw new OngoingAuthorizationException("There is already an ongoing authorization!", timerExpiryTimestamp); + } + + this.oauthClientService = oauthFactory.createOAuthClientService(email, TOKEN_URL, AUTHORIZATION_URL, clientId, + clientSecret, null, false); + this.bridgeUid = bridgeUid; + this.email = email; + redirectUri = null; + timer = null; + timerExpiryTimestamp = null; + } + + @Override + public synchronized String getAuthorizationUrl(String redirectUri) { + final OAuthClientService oauthClientService = this.oauthClientService; + if (oauthClientService == null) { + throw new NoOngoingAuthorizationException("There is no ongoing authorization!"); + } + + this.redirectUri = redirectUri; + try { + timer = scheduler.schedule(this::cancelAuthorization, AUTHORIZATION_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES); + timerExpiryTimestamp = LocalDateTime.now().plusMinutes(AUTHORIZATION_TIMEOUT_IN_MINUTES); + return oauthClientService.getAuthorizationUrl(redirectUri, null, null); + } catch (org.openhab.core.auth.client.oauth2.OAuthException e) { + abortTimer(); + cancelAuthorization(); + throw new OAuthException("Failed to determine authorization URL: " + e.getMessage(), e); + } + } + + @Override + public ThingUID getBridgeUid() { + final ThingUID bridgeUid = this.bridgeUid; + if (bridgeUid == null) { + throw new NoOngoingAuthorizationException("There is no ongoing authorization."); + } + return bridgeUid; + } + + @Override + public String getEmail() { + final String email = this.email; + if (email == null) { + throw new NoOngoingAuthorizationException("There is no ongoing authorization."); + } + return email; + } + + @Override + public synchronized void completeAuthorization(String redirectUrlWithParameters) { + abortTimer(); + + final OAuthClientService oauthClientService = this.oauthClientService; + if (oauthClientService == null) { + throw new NoOngoingAuthorizationException("There is no ongoing authorization."); + } + + try { + String authorizationCode = oauthClientService.extractAuthCodeFromAuthResponse(redirectUrlWithParameters); + + // Although this method is called "get" it actually fetches and stores the token response as a side effect. + oauthClientService.getAccessTokenResponseByAuthorizationCode(authorizationCode, redirectUri); + } catch (IOException e) { + throw new OAuthException("Network error while retrieving token response: " + e.getMessage(), e); + } catch (OAuthResponseException e) { + throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e); + } catch (org.openhab.core.auth.client.oauth2.OAuthException e) { + throw new OAuthException("Error while processing Miele service response: " + e.getMessage(), e); + } finally { + this.oauthClientService = null; + this.bridgeUid = null; + this.email = null; + this.redirectUri = null; + } + } + + /** + * Aborts the timer. + * + * Note: All calls to this method must be {@code synchronized} to ensure thread-safety. Also note that + * {@link #cancelAuthorization()} is {@code synchronized} so the execution of this method and + * {@link #cancelAuthorization()} cannot overlap. Therefore, this method is an atomic operation from the timer's + * perspective. + */ + private void abortTimer() { + final ScheduledFuture timer = this.timer; + if (timer == null) { + return; + } + + if (!timer.isDone()) { + timer.cancel(false); + } + this.timer = null; + timerExpiryTimestamp = null; + } + + private synchronized void cancelAuthorization() { + oauthClientService = null; + bridgeUid = null; + email = null; + redirectUri = null; + final ScheduledFuture timer = this.timer; + if (timer != null) { + timer.cancel(false); + this.timer = null; + timerExpiryTimestamp = null; + } + } + + @Override + public String getAccessToken(String email) { + OAuthClientService clientService = oauthFactory.getOAuthClientService(email); + if (clientService == null) { + throw new OAuthException("There is no access token registered for '" + email + "'"); + } + + try { + AccessTokenResponse response = clientService.getAccessTokenResponse(); + if (response == null) { + throw new OAuthException( + "There is no access token in the persistent storage or it already expired and could not be refreshed"); + } else { + return response.getAccessToken(); + } + } catch (org.openhab.core.auth.client.oauth2.OAuthException e) { + throw new OAuthException("Failed to read access token from persistent storage: " + e.getMessage(), e); + } catch (IOException e) { + throw new OAuthException( + "Network error during token refresh or error while reading from persistent storage: " + + e.getMessage(), + e); + } catch (OAuthResponseException e) { + throw new OAuthException("Failed to retrieve token response: " + e.getMessage(), e); + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGenerator.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGenerator.java new file mode 100644 index 0000000000000..613466ce6d181 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/ThingsTemplateGenerator.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; + +/** + * Generator for templates which can be copy-pasted into .things files by the user. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public class ThingsTemplateGenerator { + /** + * Creates a template for the bridge. + * + * @param bridgeId Id of the bridge (last part of the thing UID). + * @param locale Locale for accessing the Miele cloud service. + * @return The template. + */ + public String createBridgeConfigurationTemplate(String bridgeId, String email, String locale) { + var builder = new StringBuilder(); + builder.append("Bridge "); + builder.append(MieleCloudBindingConstants.THING_TYPE_BRIDGE.getAsString()); + builder.append(":"); + builder.append(bridgeId); + builder.append(" [ email=\""); + builder.append(email); + builder.append("\", locale=\""); + builder.append(locale); + builder.append("\" ]"); + return builder.toString(); + } + + /** + * Creates a complete template containing the bridge and all paired devices. + * + * @param bridge The bridge which is used to pair the things. + * @param pairedThings The paired things. + * @param discoveryResults The discovery results which can be paired. + * @return The template. + */ + public String createBridgeAndThingConfigurationTemplate(Bridge bridge, List pairedThings, + List discoveryResults) { + StringBuilder result = new StringBuilder(); + result.append(createBridgeConfigurationTemplate(bridge.getUID().getId(), + bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString(), + getLocale(bridge))); + result.append(" {\n"); + + for (Thing thing : pairedThings) { + result.append(" ").append(createThingConfigurationTemplate(thing)).append("\n"); + } + + for (DiscoveryResult discoveryResult : discoveryResults) { + result.append(" ").append(createThingConfigurationTemplate(discoveryResult)).append("\n"); + } + + result.append("}"); + return result.toString(); + } + + private String getLocale(Bridge bridge) { + var locale = bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE); + if (locale instanceof String) { + return (String) locale; + } else { + return "en"; + } + } + + private String createThingConfigurationTemplate(Thing thing) { + StringBuilder result = new StringBuilder(); + result.append("Thing ").append(thing.getThingTypeUID().getId()).append(" ").append(thing.getUID().getId()) + .append(" "); + + final String label = thing.getLabel(); + if (label != null) { + result.append("\"").append(label).append("\" "); + } + + result.append("[ "); + result.append("deviceIdentifier=\""); + result.append( + thing.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER).toString()); + result.append("\""); + result.append(" ]"); + return result.toString(); + } + + private String createThingConfigurationTemplate(DiscoveryResult discoveryResult) { + return "Thing " + discoveryResult.getThingTypeUID().getId() + " " + discoveryResult.getThingUID().getId() + + " \"" + discoveryResult.getLabel() + "\" [ deviceIdentifier=\"" + + getProperty(discoveryResult, MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER) + "\" ]"; + } + + private String getProperty(DiscoveryResult discoveryResult, String propertyName) { + var value = discoveryResult.getProperties().get(MieleCloudBindingConstants.CONFIG_PARAM_DEVICE_IDENTIFIER); + if (value == null) { + return ""; + } else { + return value.toString(); + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeCreationFailedException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeCreationFailedException.java new file mode 100644 index 0000000000000..54c2263674869 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeCreationFailedException.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when a bridge cannot be created in the configuration flow. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class BridgeCreationFailedException extends RuntimeException { + private static final long serialVersionUID = -6150154333256723312L; +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeReconfigurationFailedException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeReconfigurationFailedException.java new file mode 100644 index 0000000000000..97f66aae232a6 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/BridgeReconfigurationFailedException.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when reconfiguring an existing bridge fails. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public class BridgeReconfigurationFailedException extends RuntimeException { + private static final long serialVersionUID = -6341258448724364940L; + + public BridgeReconfigurationFailedException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/NoOngoingAuthorizationException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/NoOngoingAuthorizationException.java new file mode 100644 index 0000000000000..2ba3768b7148d --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/NoOngoingAuthorizationException.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when no authorization is ongoing. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public class NoOngoingAuthorizationException extends RuntimeException { + private static final long serialVersionUID = 3074275827393542416L; + + public NoOngoingAuthorizationException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/OngoingAuthorizationException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/OngoingAuthorizationException.java new file mode 100644 index 0000000000000..e232ee50db937 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/exception/OngoingAuthorizationException.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.exception; + +import java.time.LocalDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Exception thrown when there already is an ongoing authorization process. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class OngoingAuthorizationException extends RuntimeException { + private static final long serialVersionUID = -6742384930140134244L; + + @Nullable + private final LocalDateTime ongoingAuthorizationExpiryTimestamp; + + /** + * Creates a new {@link OngoingAuthorizationException}. + * + * @param message Exception message. + * @param ongoingAuthorizationExpiryTimestamp Timestamp when the ongoing authorization will expire. + */ + public OngoingAuthorizationException(String message, @Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) { + super(message); + this.ongoingAuthorizationExpiryTimestamp = ongoingAuthorizationExpiryTimestamp; + } + + /** + * Gets the timestamp representing when the ongoing authorization will expire. + */ + @Nullable + public LocalDateTime getOngoingAuthorizationExpiryTimestamp() { + return ongoingAuthorizationExpiryTimestamp; + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractRedirectionServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractRedirectionServlet.java new file mode 100644 index 0000000000000..d8a40908731d5 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractRedirectionServlet.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for servlets that have no visible frontend and just serve the purpose of redirecting the user to another + * website. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractRedirectionServlet extends HttpServlet { + private static final long serialVersionUID = 4280026301732437523L; + + private final Logger logger = LoggerFactory.getLogger(AbstractRedirectionServlet.class); + + @Override + protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) + throws ServletException, IOException { + if (response == null) { + logger.warn("Ignoring received request without response."); + return; + } + if (request == null) { + logger.warn("Ignoring illegal request."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + response.sendRedirect(getRedirectionDestination(request)); + } + + /** + * Gets the redirection destination. This can be a relative or absolute path or a link to another website. + * + * @param request The original request sent by the browser. + * @return The redirection destination. + */ + protected abstract String getRedirectionDestination(HttpServletRequest request); +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractShowPageServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractShowPageServlet.java new file mode 100644 index 0000000000000..8faa7bb2cb9f7 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AbstractShowPageServlet.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for servlets that show a visible frontend in the browser. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public abstract class AbstractShowPageServlet extends HttpServlet { + private static final long serialVersionUID = 3820684716753275768L; + + private static final String CONTENT_TYPE = "text/html;charset=UTF-8"; + + private final Logger logger = LoggerFactory.getLogger(AbstractShowPageServlet.class); + + private final ResourceLoader resourceLoader; + + protected ResourceLoader getResourceLoader() { + return resourceLoader; + } + + /** + * Creates a new {@link AbstractShowPageServlet}. + * + * @param resourceLoader Loader for resource files. + */ + public AbstractShowPageServlet(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Override + protected void doGet(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) + throws ServletException, IOException { + if (response == null) { + logger.warn("Ignoring received request without response."); + return; + } + if (request == null) { + logger.warn("Ignoring illegal request."); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + + try { + String html = handleGetRequest(request, response); + response.setContentType(CONTENT_TYPE); + response.getWriter().write(html); + response.getWriter().close(); + } catch (MieleHttpException e) { + response.sendError(e.getHttpErrorCode()); + } catch (IOException e) { + logger.warn("Failed to load resources.", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * Handles a GET request. + * + * @param request The request. + * @param response The response. + * @return A rendered HTML body to be displayed in the browser. The body will be framed by the binding's frontend + * layout. + * @throws MieleHttpException if an error occurs that should be handled by sending a default error response. + * @throws IOException if an error occurs while loading resources. + */ + protected abstract String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws MieleHttpException, IOException; +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServlet.java new file mode 100644 index 0000000000000..8944e0d3f84f8 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/AccountOverviewServlet.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants; +import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.inbox.Inbox; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; + +/** + * Servlet showing the account overview page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class AccountOverviewServlet extends AbstractShowPageServlet { + private static final long serialVersionUID = -4551210904923220429L; + private static final String ACCOUNTS_SKELETON = "index.html"; + + private static final String BRIDGES_TITLE_PLACEHOLDER = ""; + private static final String BRIDGES_PLACEHOLDER = ""; + private static final String NO_SSL_WARNING_PLACEHOLDER = ""; + + private final ThingRegistry thingRegistry; + private final Inbox inbox; + private final ThingsTemplateGenerator templateGenerator; + + /** + * Creates a new {@link AccountOverviewServlet}. + * + * @param resourceLoader Loader to use for resources. + * @param thingRegistry openHAB thing registry. + * @param inbox openHAB inbox for discovery results. + */ + public AccountOverviewServlet(ResourceLoader resourceLoader, ThingRegistry thingRegistry, Inbox inbox) { + super(resourceLoader); + this.thingRegistry = thingRegistry; + this.inbox = inbox; + this.templateGenerator = new ThingsTemplateGenerator(); + } + + @Override + protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws MieleHttpException, IOException { + String skeleton = getResourceLoader().loadResourceAsString(ACCOUNTS_SKELETON); + skeleton = renderBridges(skeleton); + skeleton = renderSslWarning(request, skeleton); + return skeleton; + } + + private String renderBridges(String skeleton) { + List bridges = thingRegistry.stream().filter(this::isMieleCloudBridge).collect(Collectors.toList()); + if (bridges.isEmpty()) { + return renderNoBridges(skeleton); + } else { + return renderBridgesIntoSkeleton(skeleton, bridges); + } + } + + private String renderNoBridges(String skeleton) { + return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "There is no account paired at the moment.") + .replace(BRIDGES_PLACEHOLDER, ""); + } + + private String renderBridgesIntoSkeleton(String skeleton, List bridges) { + StringBuilder builder = new StringBuilder(); + + int index = 0; + Iterator bridgeIterator = bridges.iterator(); + while (bridgeIterator.hasNext()) { + builder.append(renderBridge(bridgeIterator.next(), index)); + index++; + } + + return skeleton.replace(BRIDGES_TITLE_PLACEHOLDER, "The following bridges are paired") + .replace(BRIDGES_PLACEHOLDER, builder.toString()); + } + + private String renderBridge(Thing bridge, int index) { + StringBuilder builder = new StringBuilder(); + builder.append("
  • \n"); + + String thingUid = bridge.getUID().getAsString(); + String thingId = bridge.getUID().getId(); + builder.append(" "); + builder.append(thingUid.substring(0, thingUid.length() - thingId.length())); + builder.append(" "); + builder.append(thingId); + builder.append(" "); + builder.append(bridge.getConfiguration().get(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL).toString()); + builder.append("\n"); + + builder.append(" "); + builder.append(status.toString()); + builder.append("\n"); + + builder.append(" \n"); + + builder.append(" \n"); + + builder.append("
    \n"); + builder.append( + " You can use this things-file template to pair all available devices:\n"); + builder.append("
    \n"); + builder.append( + " Copy\n"); + builder.append(" \n"); + builder.append("
    \n"); + builder.append("
    \n"); + builder.append("
  • "); + + return builder.toString(); + } + + private String generateConfigurationTemplate(Bridge bridge) { + List pairedThings = thingRegistry.stream().filter(thing -> isConnectedVia(thing, bridge)) + .collect(Collectors.toList()); + List discoveryResults = inbox.stream() + .filter(discoveryResult -> willConnectVia(discoveryResult, bridge)).collect(Collectors.toList()); + + return templateGenerator.createBridgeAndThingConfigurationTemplate(bridge, pairedThings, discoveryResults); + } + + private boolean isConnectedVia(Thing thing, Bridge bridge) { + return bridge.getUID().equals(thing.getBridgeUID()); + } + + private boolean willConnectVia(DiscoveryResult discoveryResult, Bridge bridge) { + return bridge.getUID().equals(discoveryResult.getBridgeUID()); + } + + private boolean isMieleCloudBridge(Thing thing) { + return MieleCloudBindingConstants.THING_TYPE_BRIDGE.equals(thing.getThingTypeUID()); + } + + private String renderSslWarning(HttpServletRequest request, String skeleton) { + if (!request.isSecure()) { + return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, "
    \n" + + " Warning: We strongly advice to proceed only with SSL enabled for a secure data exchange.\n" + + " See Securing access to openHAB for details.\n" + + "
    "); + } else { + return skeleton.replace(NO_SSL_WARNING_PLACEHOLDER, ""); + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java new file mode 100644 index 0000000000000..3b667ce183da1 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/CreateBridgeServlet.java @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants; +import org.openhab.binding.mielecloud.internal.config.exception.BridgeCreationFailedException; +import org.openhab.binding.mielecloud.internal.config.exception.BridgeReconfigurationFailedException; +import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler; +import org.openhab.binding.mielecloud.internal.util.EmailValidator; +import org.openhab.binding.mielecloud.internal.util.LocaleValidator; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.inbox.Inbox; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingRegistry; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Servlet that automatically creates a bridge and then redirects the browser to the account overview page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class CreateBridgeServlet extends AbstractRedirectionServlet { + private static final String MIELE_CLOUD_BRIDGE_NAME = "Cloud Connector"; + private static final String MIELE_CLOUD_BRIDGE_LABEL = "Miele@home Account"; + + private static final String LOCALE_PARAMETER_NAME = "locale"; + public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid"; + public static final String EMAIL_PARAMETER_NAME = "email"; + + private static final long serialVersionUID = -2912042079128722887L; + + private static final String DEFAULT_LOCALE = "en"; + + private static final long ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS = 5000; + private static final long DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS = 5000; + private static final long CHECK_INTERVAL_IN_MILLISECONDS = 100; + + private final Logger logger = LoggerFactory.getLogger(CreateBridgeServlet.class); + + private final Inbox inbox; + private final ThingRegistry thingRegistry; + + /** + * Creates a new {@link CreateBridgeServlet}. + * + * @param inbox openHAB inbox for discovery results. + * @param thingRegistry openHAB thing registry. + */ + public CreateBridgeServlet(Inbox inbox, ThingRegistry thingRegistry) { + this.inbox = inbox; + this.thingRegistry = thingRegistry; + } + + @Override + protected String getRedirectionDestination(HttpServletRequest request) { + String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME); + if (bridgeUidString == null || bridgeUidString.isEmpty()) { + logger.warn("Cannot create bridge: Bridge UID is missing."); + return "/mielecloud/failure?" + FailureServlet.MISSING_BRIDGE_UID_PARAMETER_NAME + "=true"; + } + + String email = request.getParameter(EMAIL_PARAMETER_NAME); + if (email == null || email.isEmpty()) { + logger.warn("Cannot create bridge: E-mail address is missing."); + return "/mielecloud/failure?" + FailureServlet.MISSING_EMAIL_PARAMETER_NAME + "=true"; + } + + ThingUID bridgeUid = null; + try { + bridgeUid = new ThingUID(bridgeUidString); + } catch (IllegalArgumentException e) { + logger.warn("Cannot create bridge: Bridge UID '{}' is malformed.", bridgeUid); + return "/mielecloud/failure?" + FailureServlet.MALFORMED_BRIDGE_UID_PARAMETER_NAME + "=true"; + } + + if (!EmailValidator.isValid(email)) { + logger.warn("Cannot create bridge: E-mail address '{}' is malformed.", email); + return "/mielecloud/failure?" + FailureServlet.MALFORMED_EMAIL_PARAMETER_NAME + "=true"; + } + + String locale = getValidLocale(request.getParameter(LOCALE_PARAMETER_NAME)); + + logger.debug("Auto configuring Miele account using locale '{}' (requested locale was '{}')", locale, + request.getParameter(LOCALE_PARAMETER_NAME)); + try { + Thing bridge = pairOrReconfigureBridge(locale, bridgeUid, email); + waitForBridgeToComeOnline(bridge); + return "/mielecloud"; + } catch (BridgeReconfigurationFailedException e) { + logger.warn("{}", e.getMessage()); + return "/mielecloud/success?" + SuccessServlet.BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME + "=true&" + + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&" + + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email; + } catch (BridgeCreationFailedException e) { + logger.warn("Thing creation failed because there was no binding available that supports the thing."); + return "/mielecloud/success?" + SuccessServlet.BRIDGE_CREATION_FAILED_PARAMETER_NAME + "=true&" + + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&" + + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email; + } + } + + private Thing pairOrReconfigureBridge(String locale, ThingUID bridgeUid, String email) { + DiscoveryResult result = DiscoveryResultBuilder.create(bridgeUid) + .withRepresentationProperty(Thing.PROPERTY_MODEL_ID).withLabel(MIELE_CLOUD_BRIDGE_LABEL) + .withProperty(Thing.PROPERTY_MODEL_ID, MIELE_CLOUD_BRIDGE_NAME) + .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE, locale) + .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, email).build(); + if (inbox.add(result)) { + return pairBridge(bridgeUid); + } else { + return reconfigureBridge(bridgeUid, locale, email); + } + } + + private Thing pairBridge(ThingUID thingUid) { + Thing thing = inbox.approve(thingUid, MIELE_CLOUD_BRIDGE_LABEL, null); + if (thing == null) { + throw new BridgeCreationFailedException(); + } + + logger.debug("Successfully created bridge {}", thingUid); + return thing; + } + + private Thing reconfigureBridge(ThingUID thingUid, String locale, String email) { + logger.debug("Thing already exists. Modifying configuration."); + Thing thing = thingRegistry.get(thingUid); + if (thing == null) { + throw new BridgeReconfigurationFailedException( + "Cannot modify non existing bridge: Could neither add bridge via inbox nor find existing bridge."); + } + + ThingHandler handler = thing.getHandler(); + if (handler == null) { + throw new BridgeReconfigurationFailedException("Bridge exists but has no handler."); + } + if (!(handler instanceof MieleBridgeHandler)) { + throw new BridgeReconfigurationFailedException("Bridge handler is of wrong type, expected '" + + MieleBridgeHandler.class.getSimpleName() + "' but got '" + handler.getClass().getName() + "'."); + } + + MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) handler; + bridgeHandler.disposeWebservice(); + bridgeHandler.initializeWebservice(); + + return thing; + } + + private String getValidLocale(@Nullable String localeParameterValue) { + if (localeParameterValue == null || localeParameterValue.isEmpty() + || !LocaleValidator.isValidLanguage(localeParameterValue)) { + return DEFAULT_LOCALE; + } else { + return localeParameterValue; + } + } + + private void waitForBridgeToComeOnline(Thing bridge) { + try { + waitForConditionWithTimeout(() -> bridge.getStatus() == ThingStatus.ONLINE, + ONLINE_WAIT_TIMEOUT_IN_MILLISECONDS); + waitForConditionWithTimeout(new DiscoveryResultCountDoesNotChangeCondition(), + DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void waitForConditionWithTimeout(BooleanSupplier condition, long timeoutInMilliseconds) + throws InterruptedException { + long remainingWaitTime = timeoutInMilliseconds; + while (!condition.getAsBoolean() && remainingWaitTime > 0) { + TimeUnit.MILLISECONDS.sleep(CHECK_INTERVAL_IN_MILLISECONDS); + remainingWaitTime -= CHECK_INTERVAL_IN_MILLISECONDS; + } + } + + private class DiscoveryResultCountDoesNotChangeCondition implements BooleanSupplier { + private long previousDiscoveryResultCount = 0; + + @Override + public boolean getAsBoolean() { + var discoveryResultCount = countOwnDiscoveryResults(); + var discoveryResultCountUnchanged = previousDiscoveryResultCount == discoveryResultCount; + previousDiscoveryResultCount = discoveryResultCount; + return discoveryResultCountUnchanged; + } + + private long countOwnDiscoveryResults() { + return inbox.stream().map(DiscoveryResult::getBindingId) + .filter(MieleCloudBindingConstants.BINDING_ID::equals).count(); + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java new file mode 100644 index 0000000000000..a24802b3b298f --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/FailureServlet.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Servlet showing a failure page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public class FailureServlet extends AbstractShowPageServlet { + private static final long serialVersionUID = -5195984256535664942L; + + public static final String OAUTH2_ERROR_PARAMETER_NAME = "oauth2Error"; + public static final String ILLEGAL_RESPONSE_PARAMETER_NAME = "illegalResponse"; + public static final String NO_ONGOING_AUTHORIZATION_PARAMETER_NAME = "noOngoingAuthorization"; + public static final String FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME = "failedToCompleteAuthorization"; + public static final String MISSING_BRIDGE_UID_PARAMETER_NAME = "missingBridgeUid"; + public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail"; + public static final String MALFORMED_BRIDGE_UID_PARAMETER_NAME = "malformedBridgeUid"; + public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail"; + public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl"; + + public static final String OAUTH2_ERROR_ACCESS_DENIED = "access_denied"; + public static final String OAUTH2_ERROR_INVALID_REQUEST = "invalid_request"; + public static final String OAUTH2_ERROR_UNAUTHORIZED_CLIENT = "unauthorized_client"; + public static final String OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type"; + public static final String OAUTH2_ERROR_INVALID_SCOPE = "invalid_scope"; + public static final String OAUTH2_ERROR_SERVER_ERROR = "server_error"; + public static final String OAUTH2_ERROR_TEMPORARY_UNAVAILABLE = "temporarily_unavailable"; + + private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = ""; + + /** + * Creates a new {@link FailureServlet}. + * + * @param resourceLoader Loader to use for resources. + */ + public FailureServlet(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + @Override + protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws MieleHttpException, IOException { + return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + getErrorMessage(request)); + } + + private String getErrorMessage(HttpServletRequest request) { + String oauth2Error = request.getParameter(OAUTH2_ERROR_PARAMETER_NAME); + if (oauth2Error != null) { + return getOAuth2ErrorMessage(oauth2Error); + } else if (ServletUtil.isParameterEnabled(request, ILLEGAL_RESPONSE_PARAMETER_NAME)) { + return "Miele cloud service returned an illegal response."; + } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_PARAMETER_NAME)) { + return "There is no ongoing authorization. Please start an authorization first."; + } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME)) { + return "Completing the final authorization request failed. Please try the config flow again."; + } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_UID_PARAMETER_NAME)) { + return "Missing bridge UID."; + } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) { + return "Missing e-mail address."; + } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_UID_PARAMETER_NAME)) { + return "Malformed bridge UID."; + } else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) { + return "Malformed e-mail address."; + } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) { + return "Missing request URL. Please try the config flow again."; + } else { + return "Unknown error."; + } + } + + private String getOAuth2ErrorMessage(String oauth2Error) { + return "OAuth2 authentication with Miele cloud service failed: " + getOAuth2ErrorDetailMessage(oauth2Error); + } + + private String getOAuth2ErrorDetailMessage(String oauth2Error) { + switch (oauth2Error) { + case OAUTH2_ERROR_ACCESS_DENIED: + return "Access denied."; + case OAUTH2_ERROR_INVALID_REQUEST: + return "Malformed request."; + case OAUTH2_ERROR_UNAUTHORIZED_CLIENT: + return "Account not authorized to request authorization code."; + case OAUTH2_ERROR_UNSUPPORTED_RESPONSE_TYPE: + return "Obtaining an authorization code is not supported."; + case OAUTH2_ERROR_INVALID_SCOPE: + return "Invalid scope."; + case OAUTH2_ERROR_SERVER_ERROR: + return "Unexpected server error."; + case OAUTH2_ERROR_TEMPORARY_UNAVAILABLE: + return "Authorization server temporarily unavailable."; + default: + return "Unknown error code \"" + oauth2Error + "\"."; + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java new file mode 100644 index 0000000000000..e817463adf87d --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ForwardToLoginServlet.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants; +import org.openhab.binding.mielecloud.internal.auth.OAuthException; +import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler; +import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException; +import org.openhab.binding.mielecloud.internal.config.exception.OngoingAuthorizationException; +import org.openhab.binding.mielecloud.internal.util.EmailValidator; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Servlet gathers and processes required information to perform an authorization with the Miele cloud service + * and create a bridge afterwards. Required parameters are the client ID, client secret, an ID for the bridge and an + * e-mail address. If the given parameters are valid, the browser is redirected to the Miele service login. Otherwise, + * the browser is redirected to the previous page with an according error message. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class ForwardToLoginServlet extends AbstractRedirectionServlet { + private static final long serialVersionUID = -9094642228439994183L; + + public static final String CLIENT_ID_PARAMETER_NAME = "clientId"; + public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret"; + public static final String BRIDGE_ID_PARAMETER_NAME = "bridgeId"; + public static final String EMAIL_PARAMETER_NAME = "email"; + + private final Logger logger = LoggerFactory.getLogger(ForwardToLoginServlet.class); + + private final OAuthAuthorizationHandler authorizationHandler; + + /** + * Creates a new {@link ForwardToLoginServlet}. + * + * @param authorizationHandler Handler implementing the OAuth authorization process. + */ + public ForwardToLoginServlet(OAuthAuthorizationHandler authorizationHandler) { + this.authorizationHandler = authorizationHandler; + } + + @Override + protected String getRedirectionDestination(HttpServletRequest request) { + String clientId = request.getParameter(CLIENT_ID_PARAMETER_NAME); + String clientSecret = request.getParameter(CLIENT_SECRET_PARAMETER_NAME); + String bridgeId = request.getParameter(BRIDGE_ID_PARAMETER_NAME); + String email = request.getParameter(EMAIL_PARAMETER_NAME); + + if (clientId == null || clientId.isEmpty()) { + logger.warn("Request is missing client ID."); + return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_ID_PARAMETER_NAME); + } + if (clientSecret == null || clientSecret.isEmpty()) { + logger.warn("Request is missing client secret."); + return getErrorRedirectionUrl(PairAccountServlet.MISSING_CLIENT_SECRET_PARAMETER_NAME); + } + if (bridgeId == null || bridgeId.isEmpty()) { + logger.warn("Request is missing bridge ID."); + return getErrorRedirectionUrl(PairAccountServlet.MISSING_BRIDGE_ID_PARAMETER_NAME); + } + if (email == null || email.isEmpty()) { + logger.warn("Request is missing e-mail address."); + return getErrorRedirectionUrl(PairAccountServlet.MISSING_EMAIL_PARAMETER_NAME); + } + + ThingUID bridgeUid = null; + try { + bridgeUid = new ThingUID(MieleCloudBindingConstants.THING_TYPE_BRIDGE, bridgeId); + } catch (IllegalArgumentException e) { + logger.warn("Passed bridge ID '{}' is invalid.", bridgeId); + return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_BRIDGE_ID_PARAMETER_NAME); + } + + if (!EmailValidator.isValid(email)) { + logger.warn("Passed e-mail address '{}' is invalid.", email); + return getErrorRedirectionUrl(PairAccountServlet.MALFORMED_EMAIL_PARAMETER_NAME); + } + + try { + authorizationHandler.beginAuthorization(clientId, clientSecret, bridgeUid, email); + } catch (OngoingAuthorizationException e) { + logger.warn("Cannot begin new authorization process while another one is still running."); + return getErrorRedirectUrlWithExpiryTime(e.getOngoingAuthorizationExpiryTimestamp()); + } + + StringBuffer requestUrl = request.getRequestURL(); + if (requestUrl == null) { + return getErrorRedirectionUrl(PairAccountServlet.MISSING_REQUEST_URL_PARAMETER_NAME); + } + + try { + return authorizationHandler.getAuthorizationUrl(deriveRedirectUri(requestUrl.toString())); + } catch (NoOngoingAuthorizationException e) { + logger.warn( + "Failed to create authorization URL: There was no ongoing authorization although we just started one."); + return getErrorRedirectionUrl(PairAccountServlet.NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME); + } catch (OAuthException e) { + logger.warn("Failed to create authorization URL.", e); + return getErrorRedirectionUrl(PairAccountServlet.FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME); + } + } + + private String getErrorRedirectUrlWithExpiryTime(@Nullable LocalDateTime ongoingAuthorizationExpiryTimestamp) { + if (ongoingAuthorizationExpiryTimestamp == null) { + return getErrorRedirectionUrl( + PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME, + PairAccountServlet.ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME); + } + + long minutesUntilExpiry = ChronoUnit.MINUTES.between(LocalDateTime.now(), ongoingAuthorizationExpiryTimestamp) + + 1; + return getErrorRedirectionUrl( + PairAccountServlet.ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME, + Long.toString(minutesUntilExpiry)); + } + + private String getErrorRedirectionUrl(String errorCode) { + return getErrorRedirectionUrl(errorCode, "true"); + } + + private String getErrorRedirectionUrl(String errorCode, String parameterValue) { + return "/mielecloud/pair?" + errorCode + "=" + parameterValue; + } + + private String deriveRedirectUri(String requestUrl) { + return requestUrl + "/../result"; + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java new file mode 100644 index 0000000000000..c5eff7bc6697b --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/MieleHttpException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception wrapping a HTTP error code for further processing. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class MieleHttpException extends Exception { + private static final long serialVersionUID = 1825214275413952809L; + + private final int httpErrorCode; + + public MieleHttpException(int httpErrorCode) { + this.httpErrorCode = httpErrorCode; + } + + public int getHttpErrorCode() { + return httpErrorCode; + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java new file mode 100644 index 0000000000000..79d872a7caf4a --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/PairAccountServlet.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.io.IOException; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Servlet showing the pair account page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class PairAccountServlet extends AbstractShowPageServlet { + private static final long serialVersionUID = 6565378471951635420L; + + public static final String CLIENT_ID_PARAMETER_NAME = "clientId"; + public static final String CLIENT_SECRET_PARAMETER_NAME = "clientSecret"; + + public static final String MISSING_CLIENT_ID_PARAMETER_NAME = "missingClientId"; + public static final String MISSING_CLIENT_SECRET_PARAMETER_NAME = "missingClientSecret"; + public static final String MISSING_BRIDGE_ID_PARAMETER_NAME = "missingBridgeId"; + public static final String MISSING_EMAIL_PARAMETER_NAME = "missingEmail"; + public static final String MALFORMED_BRIDGE_ID_PARAMETER_NAME = "malformedBridgeId"; + public static final String MALFORMED_EMAIL_PARAMETER_NAME = "malformedEmail"; + public static final String FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME = "failedToDeriveRedirectUrl"; + public static final String ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME = "ongoingAuthorizationInStep1ExpiresInMinutes"; + public static final String ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME = "unknown"; + public static final String NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME = "noOngoingAuthorizationInStep2"; + public static final String MISSING_REQUEST_URL_PARAMETER_NAME = "missingRequestUrl"; + + private static final String PAIR_ACCOUNT_SKELETON = "pairing.html"; + + private static final String CLIENT_ID_PLACEHOLDER = ""; + private static final String CLIENT_SECRET_PLACEHOLDER = ""; + private static final String ERROR_MESSAGE_PLACEHOLDER = ""; + + /** + * Creates a new {@link PairAccountServlet}. + * + * @param resourceLoader Loader for resources. + */ + public PairAccountServlet(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + @Override + protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws MieleHttpException, IOException { + String skeleton = getResourceLoader().loadResourceAsString(PAIR_ACCOUNT_SKELETON); + skeleton = renderClientIdAndClientSecret(request, skeleton); + skeleton = renderErrorMessage(request, skeleton); + return skeleton; + } + + private String renderClientIdAndClientSecret(HttpServletRequest request, String skeleton) { + String prefilledClientId = Optional.ofNullable(request.getParameter(CLIENT_ID_PARAMETER_NAME)).orElse(""); + String prefilledClientSecret = Optional.ofNullable(request.getParameter(CLIENT_SECRET_PARAMETER_NAME)) + .orElse(""); + return skeleton.replace(CLIENT_ID_PLACEHOLDER, prefilledClientId).replace(CLIENT_SECRET_PLACEHOLDER, + prefilledClientSecret); + } + + private String renderErrorMessage(HttpServletRequest request, String skeleton) { + if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_ID_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Missing client ID.
    "); + } else if (ServletUtil.isParameterEnabled(request, MISSING_CLIENT_SECRET_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + + "
    Missing client secret.
    "); + } else if (ServletUtil.isParameterEnabled(request, MISSING_BRIDGE_ID_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Missing bridge ID.
    "); + } else if (ServletUtil.isParameterEnabled(request, MISSING_EMAIL_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Missing e-mail address.
    "); + } else if (ServletUtil.isParameterEnabled(request, MALFORMED_BRIDGE_ID_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Malformed bridge ID. A bridge ID may only contain letters, numbers, '-' and '_'!
    "); + } else if (ServletUtil.isParameterEnabled(request, MALFORMED_EMAIL_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Malformed e-mail address.
    "); + } else if (ServletUtil.isParameterEnabled(request, FAILED_TO_DERIVE_REDIRECT_URL_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Failed to derive redirect URL.
    "); + } else if (ServletUtil.isParameterPresent(request, + ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME)) { + String minutesUntilExpiry = request + .getParameter(ONGOING_AUTHORIZATION_IN_STEP1_EXPIRES_IN_MINUTES_PARAMETER_NAME); + if (ONGOING_AUTHORIZATION_UNKNOWN_EXPIRY_TIME.equals(minutesUntilExpiry)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again later.
    "); + } else { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    There is an authorization ongoing at the moment. Please complete that authorization prior to starting a new one or try again in " + + minutesUntilExpiry + " minutes.
    "); + } + } else if (ServletUtil.isParameterEnabled(request, NO_ONGOING_AUTHORIZATION_IN_STEP2_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Failed to start auhtorization process. Are you trying to perform multiple authorizations at the same time?
    "); + } else if (ServletUtil.isParameterEnabled(request, MISSING_REQUEST_URL_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, + "
    Missing request URL. Please try again.
    "); + } else { + return skeleton.replace(ERROR_MESSAGE_PLACEHOLDER, ""); + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java new file mode 100644 index 0000000000000..d93a3c9999f19 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResourceLoader.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.osgi.framework.BundleContext; + +/** + * Provides access to resource files for servlets. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class ResourceLoader { + private static final String BEGINNING_OF_INPUT = "\\A"; + + private final String basePath; + private final BundleContext bundleContext; + + /** + * Creates a new {@link ResourceLoader}. + * + * @param basePath The base path to use for loading. A trailing {@code "/"} is removed. + * @param bundleContext {@link BundleContext} to load from. + */ + public ResourceLoader(String basePath, BundleContext bundleContext) { + this.basePath = removeTrailingSlashes(basePath); + this.bundleContext = bundleContext; + } + + private String removeTrailingSlashes(String value) { + String ret = value; + while (ret.endsWith("/")) { + ret = ret.substring(0, ret.length() - 1); + } + return ret; + } + + /** + * Opens a resource relative to the base path. + * + * @param filename The filename of the resource to load. + * @return A stream reading from the resource file. + * @throws FileNotFoundException If the requested resource file cannot be found. + * @throws IOException If an error occurs while opening a stream to the resource. + */ + public InputStream openResource(String filename) throws IOException { + URL url = bundleContext.getBundle().getEntry(basePath + "/" + filename); + if (url == null) { + throw new FileNotFoundException("Cannot find '" + filename + "' relative to '" + basePath + "'"); + } + + return url.openStream(); + } + + /** + * Loads the contents of a resource file as UTF-8 encoded {@link String}. + * + * @param filename The filename of the resource to load. + * @return The contents of the file. + * @throws FileNotFoundException If the requested resource file cannot be found. + * @throws IOException If an error occurs while opening a stream to the resource or reading from it. + */ + public String loadResourceAsString(String filename) throws IOException { + try (Scanner scanner = new Scanner(openResource(filename), StandardCharsets.UTF_8.name())) { + return scanner.useDelimiter(BEGINNING_OF_INPUT).next(); + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java new file mode 100644 index 0000000000000..5a5db09090967 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ResultServlet.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mielecloud.internal.auth.OAuthException; +import org.openhab.binding.mielecloud.internal.config.OAuthAuthorizationHandler; +import org.openhab.binding.mielecloud.internal.config.exception.NoOngoingAuthorizationException; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Servlet processing the response by the Miele service after a login. This servlet is called as a result of a + * completed login to the Miele service and assumes that the OAuth 2 parameters are passed. Depending on the parameters + * and whether the token response can be fetched either the browser is redirected to the success or the failure page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class ResultServlet extends AbstractRedirectionServlet { + private static final long serialVersionUID = 2157912755568949550L; + + public static final String CODE_PARAMETER_NAME = "code"; + public static final String STATE_PARAMETER_NAME = "state"; + public static final String ERROR_PARAMETER_NAME = "error"; + + private final Logger logger = LoggerFactory.getLogger(ResultServlet.class); + + private final OAuthAuthorizationHandler authorizationHandler; + + /** + * Creates a new {@link ResultServlet}. + * + * @param authorizationHandler Handler implementing the OAuth authorization. + */ + public ResultServlet(OAuthAuthorizationHandler authorizationHandler) { + this.authorizationHandler = authorizationHandler; + } + + @Override + protected String getRedirectionDestination(HttpServletRequest request) { + String error = request.getParameter(ERROR_PARAMETER_NAME); + if (error != null) { + logger.warn("Received error response: {}", error); + return "/mielecloud/failure?" + FailureServlet.OAUTH2_ERROR_PARAMETER_NAME + "=" + error; + } + + String code = request.getParameter(CODE_PARAMETER_NAME); + if (code == null) { + logger.warn("Code is null"); + return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true"; + } + String state = request.getParameter(STATE_PARAMETER_NAME); + if (state == null) { + logger.warn("State is null"); + return "/mielecloud/failure?" + FailureServlet.ILLEGAL_RESPONSE_PARAMETER_NAME + "=true"; + } + + try { + ThingUID bridgeId = authorizationHandler.getBridgeUid(); + String email = authorizationHandler.getEmail(); + + StringBuffer requestUrl = request.getRequestURL(); + if (requestUrl == null) { + return "/mielecloud/failure?" + FailureServlet.MISSING_REQUEST_URL_PARAMETER_NAME + "=true"; + } + + try { + authorizationHandler.completeAuthorization(requestUrl.toString() + "?" + request.getQueryString()); + } catch (OAuthException e) { + logger.warn("Failed to complete authorization.", e); + return "/mielecloud/failure?" + FailureServlet.FAILED_TO_COMPLETE_AUTHORIZATION_PARAMETER_NAME + + "=true"; + } + + return "/mielecloud/success?" + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeId.getAsString() + + "&" + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email; + } catch (NoOngoingAuthorizationException e) { + logger.warn("Failed to complete authorization: There is no ongoing authorization or it timed out"); + return "/mielecloud/failure?" + FailureServlet.NO_ONGOING_AUTHORIZATION_PARAMETER_NAME + "=true"; + } + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java new file mode 100644 index 0000000000000..4441aca3d8d8a --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/ServletUtil.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Utility class for common servlet tasks. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public final class ServletUtil { + private ServletUtil() { + throw new UnsupportedOperationException(); + } + + /** + * Gets the value of a request parameter or returns a default if the parameter is not present. + */ + public static String getParameterValueOrDefault(HttpServletRequest request, String parameterName, + String defaultValue) { + String parameterValue = request.getParameter(parameterName); + if (parameterValue == null) { + return defaultValue; + } else { + return parameterValue; + } + } + + /** + * Checks whether a request parameter is enabled. + */ + public static boolean isParameterEnabled(HttpServletRequest request, String parameterName) { + return "true".equalsIgnoreCase(getParameterValueOrDefault(request, parameterName, "false")); + } + + /** + * Checks whether a parameter is present in a request. + */ + public static boolean isParameterPresent(HttpServletRequest request, String parameterName) { + String parameterValue = request.getParameter(parameterName); + return parameterValue != null && !parameterValue.trim().isEmpty(); + } +} diff --git a/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java new file mode 100644 index 0000000000000..d240f215ee738 --- /dev/null +++ b/bundles/org.openhab.binding.mielecloud/src/main/java/org/openhab/binding/mielecloud/internal/config/servlet/SuccessServlet.java @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2010-2021 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.mielecloud.internal.config.servlet; + +import java.io.IOException; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mielecloud.internal.config.ThingsTemplateGenerator; +import org.openhab.binding.mielecloud.internal.util.EmailValidator; +import org.openhab.binding.mielecloud.internal.webservice.language.LanguageProvider; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Servlet showing the success page. + * + * @author Björn Lange - Initial Contribution + */ +@NonNullByDefault +public class SuccessServlet extends AbstractShowPageServlet { + private static final long serialVersionUID = 7013060161686096950L; + + public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid"; + public static final String EMAIL_PARAMETER_NAME = "email"; + + public static final String BRIDGE_CREATION_FAILED_PARAMETER_NAME = "bridgeCreationFailed"; + public static final String BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME = "bridgeReconfigurationFailed"; + + private static final String ERROR_MESSAGE_TEXT_PLACEHOLDER = ""; + private static final String BRIDGE_UID_PLACEHOLDER = ""; + private static final String EMAIL_PLACEHOLDER = ""; + private static final String THINGS_TEMPLATE_CODE_PLACEHOLDER = ""; + + private static final String LOCALE_OPTIONS_PLACEHOLDER = ""; + + private static final String DEFAULT_LANGUAGE = "en"; + private static final Set SUPPORTED_LANGUAGES = Set.of("da", "nl", "en", "fr", "de", "it", "nb", "es"); + + private final Logger logger = LoggerFactory.getLogger(SuccessServlet.class); + + private final LanguageProvider languageProvider; + private final ThingsTemplateGenerator templateGenerator; + + /** + * Creates a new {@link SuccessServlet}. + * + * @param resourceLoader Loader for resources. + * @param languageProvider Provider for the language to use as default selection. + */ + public SuccessServlet(ResourceLoader resourceLoader, LanguageProvider languageProvider) { + super(resourceLoader); + this.languageProvider = languageProvider; + this.templateGenerator = new ThingsTemplateGenerator(); + } + + @Override + protected String handleGetRequest(HttpServletRequest request, HttpServletResponse response) + throws MieleHttpException, IOException { + String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME); + if (bridgeUidString == null || bridgeUidString.isEmpty()) { + logger.warn("Success page is missing bridge UID."); + return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + "Missing bridge UID."); + } + + String email = request.getParameter(EMAIL_PARAMETER_NAME); + if (email == null || email.isEmpty()) { + logger.warn("Success page is missing e-mail address."); + return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + "Missing e-mail address."); + } + + ThingUID bridgeUid = null; + try { + bridgeUid = new ThingUID(bridgeUidString); + } catch (IllegalArgumentException e) { + logger.warn("Success page received malformed bridge UID '{}'.", bridgeUidString); + return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + "Malformed bridge UID."); + } + + if (!EmailValidator.isValid(email)) { + logger.warn("Success page received malformed e-mail address '{}'.", email); + return getResourceLoader().loadResourceAsString("failure.html").replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + "Malformed e-mail address."); + } + + String skeleton = getResourceLoader().loadResourceAsString("success.html"); + skeleton = renderErrorMessage(request, skeleton); + skeleton = renderBridgeUid(skeleton, bridgeUid); + skeleton = renderEmail(skeleton, email); + skeleton = renderLocaleSelection(skeleton); + skeleton = renderBridgeConfigurationTemplate(skeleton, bridgeUid, email); + return skeleton; + } + + private String renderErrorMessage(HttpServletRequest request, String skeleton) { + if (ServletUtil.isParameterEnabled(request, BRIDGE_CREATION_FAILED_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + "
    Could not auto configure the bridge. Failed to approve the bridge from the inbox. Please try the configuration flow again.
    "); + } else if (ServletUtil.isParameterEnabled(request, BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME)) { + return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, + "
    Could not auto reconfigure the bridge. Bridge thing or thing handler is not available. Please try the configuration flow again.
    "); + } else { + return skeleton.replace(ERROR_MESSAGE_TEXT_PLACEHOLDER, ""); + } + } + + private String renderBridgeUid(String skeleton, ThingUID bridgeUid) { + return skeleton.replace(BRIDGE_UID_PLACEHOLDER, bridgeUid.getAsString()); + } + + private String renderEmail(String skeleton, String email) { + return skeleton.replace(EMAIL_PLACEHOLDER, email); + } + + private String renderLocaleSelection(String skeleton) { + String preSelectedLanguage = languageProvider.getLanguage().filter(SUPPORTED_LANGUAGES::contains) + .orElse(DEFAULT_LANGUAGE); + + return skeleton.replace(LOCALE_OPTIONS_PLACEHOLDER, + SUPPORTED_LANGUAGES.stream().map(Language::fromCode).filter(Optional::isPresent).map(Optional::get) + .sorted() + .map(language -> createOptionTag(language, preSelectedLanguage.equals(language.getCode()))) + .collect(Collectors.joining("\n"))); + } + + private String createOptionTag(Language language, boolean selected) { + String firstPart = "