[pwm] Initial Contribution (#10205)

Signed-off-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
Fabian Wolter 2021-06-01 20:23:33 +02:00 committed by GitHub
parent cf34ba23a0
commit 9d903c240e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1314 additions and 0 deletions

View 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

View File

@ -36,6 +36,11 @@
<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>

View File

@ -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

View File

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View 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>