]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pwm] Initial Contribution (#10205)
authorFabian Wolter <github@fabian-wolter.de>
Tue, 1 Jun 2021 18:23:33 +0000 (20:23 +0200)
committerGitHub <noreply@github.com>
Tue, 1 Jun 2021 18:23:33 +0000 (20:23 +0200)
Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
25 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.automation.pwm/NOTICE [new file with mode: 0644]
bundles/org.openhab.automation.pwm/README.md [new file with mode: 0644]
bundles/org.openhab.automation.pwm/doc/statemachine.odg [new file with mode: 0644]
bundles/org.openhab.automation.pwm/doc/statemachine.png [new file with mode: 0644]
bundles/org.openhab.automation.pwm/pom.xml [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/PWMConstants.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/PWMException.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/factory/PWMModuleHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/PWMTriggerHandler.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/AlwaysOffState.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/AlwaysOnState.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/DutycycleHundredState.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/DutycycleZeroState.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/OffState.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/OnState.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/State.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/handler/state/StateMachine.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/template/PWMRuleTemplate.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/template/PWMTemplateProvider.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/type/PWMModuleTypeProvider.java [new file with mode: 0644]
bundles/org.openhab.automation.pwm/src/main/java/org/openhab/automation/pwm/internal/type/PWMTriggerType.java [new file with mode: 0644]
bundles/pom.xml

index ca38e94d9a3e5c22bc98acc7f2a04cdfb957006c..f9b28129999e565c783ea8c49c9f9e2eebe53dad 100644 (file)
@@ -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
index b183a255f0598f5fbc68215e727c04bf1b34fab8..defb5cbeded19f1ec28e63583285378746c551f8 100644 (file)
       <artifactId>org.openhab.automation.pidcontroller</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.automation.pwm</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.adorne</artifactId>
diff --git a/bundles/org.openhab.automation.pwm/NOTICE b/bundles/org.openhab.automation.pwm/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..840b19d
--- /dev/null
@@ -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 (file)
index 0000000..be28e78
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 (file)
index 0000000..aa99d12
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 (file)
index 0000000..d972adb
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.automation.pwm</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Automation :: PWM</name>
+
+</project>
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 (file)
index 0000000..212e8c2
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.automation.pwm-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-automation-pwm" description="PWM Automation" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.automation.pwm/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..e207232
--- /dev/null
@@ -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 (file)
index 0000000..8b2f86b
--- /dev/null
@@ -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 (file)
index 0000000..87e54e0
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index 0000000..f5c1619
--- /dev/null
@@ -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<String> 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<Double> minDutyCycle;
+    private final Optional<Double> maxDutyCycle;
+    private final Optional<Double> 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<Double> 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<String> 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 (file)
index 0000000..e8e21be
--- /dev/null
@@ -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 (file)
index 0000000..53d49c0
--- /dev/null
@@ -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 (file)
index 0000000..121549c
--- /dev/null
@@ -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 (file)
index 0000000..59e3a12
--- /dev/null
@@ -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 (file)
index 0000000..0762d2d
--- /dev/null
@@ -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 (file)
index 0000000..e1c22c2
--- /dev/null
@@ -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 (file)
index 0000000..2bf490b
--- /dev/null
@@ -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<StateMachine, ? extends State> 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 (file)
index 0000000..47c8454
--- /dev/null
@@ -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<Boolean> controlOutput;
+    private State state;
+    private long periodMs;
+    private double dutycycle;
+
+    public StateMachine(ScheduledExecutorService scheduler, Consumer<Boolean> 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 (file)
index 0000000..cf715d7
--- /dev/null
@@ -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<Trigger> triggers = Collections.singletonList(ModuleBuilder.createTrigger().withId(triggerId)
+                .withTypeUID(PWMTriggerType.UID).withLabel("PWM Trigger").build());
+
+        final Map<String, String> actionInputs = new HashMap<String, String>();
+        actionInputs.put(PWMConstants.INPUT, triggerId + "." + PWMConstants.OUTPUT);
+
+        Set<String> tags = new HashSet<String>();
+        tags.add("PWM");
+
+        return new PWMRuleTemplate(tags, triggers, Collections.emptyList(), Collections.emptyList(),
+                Collections.emptyList());
+    }
+
+    public PWMRuleTemplate(Set<String> tags, List<Trigger> triggers, List<Condition> conditions, List<Action> actions,
+            List<ConfigDescriptionParameter> 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 (file)
index 0000000..87fc455
--- /dev/null
@@ -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<String, RuleTemplate> providedRuleTemplates = new HashMap<String, RuleTemplate>();
+
+    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<RuleTemplate> getTemplates(@Nullable Locale locale) {
+        return providedRuleTemplates.values();
+    }
+
+    @Override
+    public void addProviderChangeListener(ProviderChangeListener<RuleTemplate> listener) {
+        // does nothing because this provider does not change
+    }
+
+    @Override
+    public Collection<RuleTemplate> getAll() {
+        return Collections.unmodifiableCollection(providedRuleTemplates.values());
+    }
+
+    @Override
+    public void removeProviderChangeListener(ProviderChangeListener<RuleTemplate> 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 (file)
index 0000000..2db1492
--- /dev/null
@@ -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<String, ModuleType> PROVIDED_MODULE_TYPES = Map.of(PWMTriggerHandler.MODULE_TYPE_ID,
+            PWMTriggerType.initialize());
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends ModuleType> T getModuleType(@Nullable String UID, @Nullable Locale locale) {
+        return (T) PROVIDED_MODULE_TYPES.get(UID);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends ModuleType> Collection<T> getModuleTypes(@Nullable Locale locale) {
+        return (Collection<T>) PROVIDED_MODULE_TYPES.values();
+    }
+
+    @Override
+    public void addProviderChangeListener(ProviderChangeListener<ModuleType> listener) {
+        // does nothing because this provider does not change
+    }
+
+    @Override
+    public Collection<ModuleType> getAll() {
+        return Collections.unmodifiableCollection(PROVIDED_MODULE_TYPES.values());
+    }
+
+    @Override
+    public void removeProviderChangeListener(ProviderChangeListener<ModuleType> 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 (file)
index 0000000..f085932
--- /dev/null
@@ -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<ConfigDescriptionParameter> 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<Output> 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<ConfigDescriptionParameter> configDescriptions, List<Output> outputs) {
+        super(UID, configDescriptions, "PWM triggers", null, null, Visibility.VISIBLE, outputs);
+    }
+}
index fef76662048d3d820e7713fc19c18a470db1738a..47f28d1587ffcbad82f15785aef130c2cd4590fe 100644 (file)
@@ -22,6 +22,7 @@
     <module>org.openhab.automation.jsscripting</module>
     <module>org.openhab.automation.jythonscripting</module>
     <module>org.openhab.automation.pidcontroller</module>
+    <module>org.openhab.automation.pwm</module>
     <!-- io -->
     <module>org.openhab.io.homekit</module>
     <module>org.openhab.io.hueemulation</module>