diff --git a/CODEOWNERS b/CODEOWNERS
index 8ffad8e5199..5462def510b 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -97,6 +97,7 @@
/bundles/org.openhab.binding.gpstracker/ @gbicskei
/bundles/org.openhab.binding.gree/ @markus7017
/bundles/org.openhab.binding.groheondus/ @FlorianSW
+/bundles/org.openhab.binding.haassohnpelletstove/ @chingon007
/bundles/org.openhab.binding.harmonyhub/ @digitaldan
/bundles/org.openhab.binding.haywardomnilogic/ @matchews
/bundles/org.openhab.binding.hdanywhere/ @kgoderis
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 385c85c2d40..10205adc0c2 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -471,6 +471,11 @@
org.openhab.binding.groheondus
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.haassohnpelletstove
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.harmonyhub
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/NOTICE b/bundles/org.openhab.binding.haassohnpelletstove/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/README.md b/bundles/org.openhab.binding.haassohnpelletstove/README.md
new file mode 100644
index 00000000000..2d053f48152
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/README.md
@@ -0,0 +1,69 @@
+# Haas Sohn Pellet Stove Binding
+
+The binding for Haassohnpelletstove communicates with a Haas and Sohn Pelletstove through the optional
+WIFI module. More information about the WIFI module can be found here: https://www.haassohn.com/de/ihr-plus/WLAN-Funktion
+
+## Supported Things
+
+| Things | Description | Thing Type |
+|--------|--------------|------------|
+| haassohnpelletstove | Control of a Haas & Sohn Pellet Stove| oven|
+
+
+## Thing Configuration
+
+In general two parameters are required. The IP-Address of the WIFI-Modul of the Stove in the local Network and the Access PIN of the Stove.
+The PIN can be found directly at the stove under the Menue/Network/WLAN-PIN
+
+```
+Thing haassohnpelletstove:oven:myOven "Pelletstove" [ hostIP="192.168.0.23", hostPIN="1234"]
+```
+
+## Channels
+
+The following channels are yet supported:
+
+
+| Channel | Type | Access| Description|
+|---------|-------|-------|------------|
+| power| Switch | read/write|Turn the stove on/off|
+|channelIsTemp|Number:Temperature|read|Receives the actual temperature of the stove|
+|channelSpTemp|Number:Temperature|read/write|Receives and sets the target temperature of the stove|
+|channelMode|String|read|Receives the actual mode the stove is in like heating, cooling, error, ....|
+|channelEcoMode|Switch|read/write|Turn the eco mode of the stove on/off|
+|channelIngitions|Number|read|Amount of ignitions of the stove|
+|channelMaintenanceIn|Number:Mass|read|States the next maintenance in kg|
+|channelCleaningIn|String|read|States the next cleaning window in hours:minutes as string|
+|channelConsumption|Number:Mass|read|Total consumption of the stove|
+|channelOnTime|Number|read|Operation hours of the stove|
+
+## Full Example
+
+demo.items:
+
+```
+Number:Temperature isTemp { channel="oven:channelIsTemp" }
+Number:Temperature spTemp { channel="oven:channelSpTemp" }
+String mode { channel="oven:channelMode" }
+Switch power { channel="oven:power" }
+```
+
+## Google Assistant configuration
+
+See also: https://www.openhab.org/docs/ecosystem/google-assistant/
+
+googleassistantdemo.items
+
+```
+Group g_FeuerThermostat "FeuerThermostat" {ga="Thermostat" }
+Number StatusFeuer "Status Feuer" (g_FeuerThermostat) { ga="thermostatMode" }
+Number ZieltemperaturFeuer "ZieltemperaturFeuer" (g_FeuerThermostat) {ga="thermostatTemperatureSetpoint"}
+Number TemperaturFeuer "TemperaturFeuer" (g_FeuerThermostat) {ga="thermostatTemperatureAmbient"}
+```
+
+## Tested Hardware
+
+The binding was successfully tested with the following ovens:
+
+- HSP 7 DIANA
+- HSP6 434.08
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/pom.xml b/bundles/org.openhab.binding.haassohnpelletstove/pom.xml
new file mode 100644
index 00000000000..d7aed0af57f
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.haassohnpelletstove
+
+ openHAB Add-ons :: Bundles :: Haas + Sohn Pelletstove Binding
+
+
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/feature/feature.xml b/bundles/org.openhab.binding.haassohnpelletstove/src/main/feature/feature.xml
new file mode 100644
index 00000000000..6861266bbfa
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.haassohnpelletstove/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveBindingConstants.java b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveBindingConstants.java
new file mode 100644
index 00000000000..e60f57d9573
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveBindingConstants.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.haassohnpelletstove.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link HaasSohnpelletstoveBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Christian Feininger - Initial contribution
+ */
+@NonNullByDefault
+public class HaasSohnpelletstoveBindingConstants {
+
+ private static final String BINDING_ID = "haassohnpelletstove";
+
+ public static final ThingTypeUID THING_TYPE_OVEN = new ThingTypeUID(BINDING_ID, "oven");
+
+ public static final String CHANNELISTEMP = "channelIsTemp";
+ public static final String CHANNELMODE = "channelMode";
+ public static final String CHANNELSPTEMP = "channelSpTemp";
+ public static final String CHANNELPOWER = "power";
+ public static final String CHANNELECOMODE = "channelEcoMode";
+ public static final String CHANNELIGNITIONS = "channelIgnitions";
+ public static final String CHANNELMAINTENANCEIN = "channelMaintenanceIn";
+ public static final String CHANNELCLEANINGIN = "channelCleaningIn";
+ public static final String CHANNELCONSUMPTION = "channelConsumption";
+ public static final String CHANNELONTIME = "channelOnTime";
+}
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveConfiguration.java b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveConfiguration.java
new file mode 100644
index 00000000000..cfb6e3c1a44
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveConfiguration.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.haassohnpelletstove.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link HaasSohnpelletstoveConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Christian Feininger - Initial contribution
+ */
+@NonNullByDefault
+public class HaasSohnpelletstoveConfiguration {
+
+ public @Nullable String hostIP = null;
+ public @Nullable String hostPIN = null;
+ public int refreshRate = 30;
+}
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveHandler.java b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveHandler.java
new file mode 100644
index 00000000000..a8c28a108c4
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveHandler.java
@@ -0,0 +1,318 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.haassohnpelletstove.internal;
+
+import static org.openhab.binding.haassohnpelletstove.internal.HaasSohnpelletstoveBindingConstants.*;
+
+import java.text.DecimalFormat;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link HaasSohnpelletstoveHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Christian Feininger - Initial contribution
+ */
+@NonNullByDefault
+public class HaasSohnpelletstoveHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(HaasSohnpelletstoveHandler.class);
+
+ private @Nullable ScheduledFuture> refreshJob;
+
+ private HaasSohnpelletstoveConfiguration config = new HaasSohnpelletstoveConfiguration();
+ boolean resultOk = false;
+
+ private HaasSohnpelletstoveJSONCommunication serviceCommunication;
+
+ private boolean automaticRefreshing = false;
+
+ private Map linkedChannels = new HashMap();
+
+ public HaasSohnpelletstoveHandler(Thing thing) {
+ super(thing);
+ serviceCommunication = new HaasSohnpelletstoveJSONCommunication();
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (channelUID.getId().equals(CHANNELPOWER)) {
+ String postData = null;
+ if (command.equals(OnOffType.ON)) {
+ postData = "{\"prg\":true}";
+ } else if (command.equals(OnOffType.OFF)) {
+ postData = "{\"prg\":false}";
+ }
+ if (postData != null) {
+ logger.debug("Executing {} command", CHANNELPOWER);
+ updateOvenData(postData);
+ }
+ } else if (channelUID.getId().equals(CHANNELSPTEMP)) {
+ if (command instanceof QuantityType>) {
+ QuantityType> value = (QuantityType>) command;
+
+ Unit unit = SIUnits.CELSIUS;
+ value = value.toUnit(unit);
+ if (value != null) {
+ double a = value.doubleValue();
+ String postdata = "{\"sp_temp\":" + a + "}";
+ logger.debug("Executing {} command", CHANNELSPTEMP);
+ updateOvenData(postdata);
+ }
+ } else {
+ logger.debug("Error. Command is the wrong type: {}", command.toString());
+ }
+ } else if (channelUID.getId().equals(CHANNELECOMODE)) {
+ String postData = null;
+ if (command.equals(OnOffType.ON)) {
+ postData = "{\"eco_mode\":true}";
+ } else if (command.equals(OnOffType.OFF)) {
+ postData = "{\"eco_mode\":false}";
+ }
+ if (postData != null) {
+ logger.debug("Executing {} command", CHANNELECOMODE);
+ updateOvenData(postData);
+ }
+ }
+ }
+
+ /**
+ * Calls the service to update the oven data
+ *
+ * @param postdata
+ */
+ private boolean updateOvenData(@Nullable String postdata) {
+ Helper message = new Helper();
+ if (serviceCommunication.updateOvenData(postdata, message, this.getThing().getUID().toString())) {
+ updateStatus(ThingStatus.ONLINE);
+ return true;
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ message.getStatusDesription());
+ return false;
+ }
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("Initializing haassohnpelletstove handler for thing {}", getThing().getUID());
+ config = getConfigAs(HaasSohnpelletstoveConfiguration.class);
+ boolean validConfig = true;
+ String errors = "";
+ String statusDescr = null;
+ if (config.refreshRate < 0 && config.refreshRate > 999) {
+ errors += " Parameter 'refresh Rate' greater then 0 and less then 1000.";
+ statusDescr = "Parameter 'refresh Rate' greater then 0 and less then 1000.";
+ validConfig = false;
+ }
+ if (config.hostIP == null) {
+ errors += " Parameter 'hostIP' must be configured.";
+ statusDescr = "IP Address must be configured!";
+ validConfig = false;
+ }
+ if (config.hostPIN == null) {
+ errors += " Parameter 'hostPin' must be configured.";
+ statusDescr = "PIN must be configured!";
+ validConfig = false;
+ }
+ errors = errors.trim();
+ Helper message = new Helper();
+ message.setStatusDescription(statusDescr);
+ if (validConfig) {
+ serviceCommunication.setConfig(config);
+ if (serviceCommunication.refreshOvenConnection(message, this.getThing().getUID().toString())) {
+ if (updateOvenData(null)) {
+ updateStatus(ThingStatus.ONLINE);
+ updateLinkedChannels();
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message.getStatusDesription());
+ }
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message.getStatusDesription());
+ }
+ }
+
+ private void updateLinkedChannels() {
+ verifyLinkedChannel(CHANNELISTEMP);
+ verifyLinkedChannel(CHANNELMODE);
+ verifyLinkedChannel(CHANNELPOWER);
+ verifyLinkedChannel(CHANNELSPTEMP);
+ verifyLinkedChannel(CHANNELECOMODE);
+ verifyLinkedChannel(CHANNELIGNITIONS);
+ verifyLinkedChannel(CHANNELMAINTENANCEIN);
+ verifyLinkedChannel(CHANNELCLEANINGIN);
+ verifyLinkedChannel(CHANNELCONSUMPTION);
+ verifyLinkedChannel(CHANNELONTIME);
+ if (!linkedChannels.isEmpty()) {
+ updateOvenData(null);
+ for (Channel channel : getThing().getChannels()) {
+ updateChannel(channel.getUID().getId());
+ }
+ startAutomaticRefresh();
+ automaticRefreshing = true;
+ }
+ }
+
+ private void verifyLinkedChannel(String channelID) {
+ if (isLinked(channelID) && !linkedChannels.containsKey(channelID)) {
+ linkedChannels.put(channelID, true);
+ }
+ }
+
+ @Override
+ public void dispose() {
+ stopScheduler();
+ }
+
+ private void stopScheduler() {
+ ScheduledFuture> job = refreshJob;
+ if (job != null) {
+ job.cancel(true);
+ }
+ refreshJob = null;
+ }
+
+ /**
+ * Start the job refreshing the oven status
+ */
+ private void startAutomaticRefresh() {
+ ScheduledFuture> job = refreshJob;
+ if (job == null || job.isCancelled()) {
+ int period = config.refreshRate;
+ refreshJob = scheduler.scheduleWithFixedDelay(this::run, 0, period, TimeUnit.SECONDS);
+ }
+ }
+
+ private void run() {
+ updateOvenData(null);
+ for (Channel channel : getThing().getChannels()) {
+ updateChannel(channel.getUID().getId());
+ }
+ }
+
+ @Override
+ public void channelLinked(ChannelUID channelUID) {
+ if (!automaticRefreshing) {
+ logger.debug("Start automatic refreshing");
+ startAutomaticRefresh();
+ automaticRefreshing = true;
+ }
+ verifyLinkedChannel(channelUID.getId());
+ updateChannel(channelUID.getId());
+ }
+
+ @Override
+ public void channelUnlinked(ChannelUID channelUID) {
+ linkedChannels.remove(channelUID.getId());
+ if (linkedChannels.isEmpty()) {
+ automaticRefreshing = false;
+ stopScheduler();
+ logger.debug("Stop automatic refreshing");
+ }
+ }
+
+ private void updateChannel(String channelId) {
+ if (isLinked(channelId)) {
+ State state = null;
+ HaasSohnpelletstoveJsonDataDTO data = serviceCommunication.getOvenData();
+ if (data != null) {
+ switch (channelId) {
+ case CHANNELISTEMP:
+ state = new QuantityType(Double.valueOf(data.getisTemp()), SIUnits.CELSIUS);
+ update(state, channelId);
+ break;
+ case CHANNELMODE:
+ state = new StringType(data.getMode());
+ update(state, channelId);
+ break;
+ case CHANNELPOWER:
+ update(OnOffType.from(data.getPrg()), channelId);
+ break;
+ case CHANNELECOMODE:
+ update(OnOffType.from(data.getEcoMode()), channelId);
+ break;
+ case CHANNELSPTEMP:
+ state = new QuantityType(Double.valueOf(data.getspTemp()), SIUnits.CELSIUS);
+ update(state, channelId);
+ break;
+ case CHANNELCLEANINGIN:
+ String cleaning = data.getCleaningIn();
+ double time = Double.parseDouble(cleaning);
+ time = time / 60;
+ DecimalFormat df = new DecimalFormat("0.00");
+ state = new StringType(df.format(time));
+ update(state, channelId);
+ break;
+ case CHANNELCONSUMPTION:
+ state = new StringType(data.getConsumption());
+ update(state, channelId);
+ break;
+ case CHANNELIGNITIONS:
+ state = new StringType(data.getIgnitions());
+ update(state, channelId);
+ break;
+ case CHANNELMAINTENANCEIN:
+ state = new StringType(data.getMaintenanceIn());
+ update(state, channelId);
+ break;
+ case CHANNELONTIME:
+ state = new StringType(data.getOnTime());
+ update(state, channelId);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the State of the given channel
+ *
+ * @param state
+ * @param channelId
+ */
+ private void update(@Nullable State state, String channelId) {
+ logger.debug("Update channel {} with state {}", channelId, (state == null) ? "null" : state.toString());
+
+ if (state != null) {
+ updateState(channelId, state);
+
+ } else {
+ updateState(channelId, UnDefType.NULL);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveHandlerFactory.java b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveHandlerFactory.java
new file mode 100644
index 00000000000..165b5105cf6
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveHandlerFactory.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.haassohnpelletstove.internal;
+
+import static org.openhab.binding.haassohnpelletstove.internal.HaasSohnpelletstoveBindingConstants.THING_TYPE_OVEN;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link HaasSohnpelletstoveHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Christian Feininger - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.haassohnpelletstove", service = ThingHandlerFactory.class)
+public class HaasSohnpelletstoveHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_OVEN);
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_OVEN.equals(thingTypeUID)) {
+ return new HaasSohnpelletstoveHandler(thing);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveJSONCommunication.java b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveJSONCommunication.java
new file mode 100644
index 00000000000..04c20597855
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveJSONCommunication.java
@@ -0,0 +1,223 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.haassohnpelletstove.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Properties;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.io.net.http.HttpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * This class handles the JSON communication with the Wifi Modul of the Stove
+ *
+ * @author Christian Feininger - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class HaasSohnpelletstoveJSONCommunication {
+
+ private final Logger logger = LoggerFactory.getLogger(HaasSohnpelletstoveJSONCommunication.class);
+ private HaasSohnpelletstoveConfiguration config;
+
+ private Gson gson;
+ private @Nullable String xhspin;
+ private @Nullable HaasSohnpelletstoveJsonDataDTO ovenData;
+
+ public HaasSohnpelletstoveJSONCommunication() {
+ gson = new Gson();
+ ovenData = new HaasSohnpelletstoveJsonDataDTO();
+ xhspin = "";
+ config = new HaasSohnpelletstoveConfiguration();
+ }
+
+ /**
+ * Refreshes the oven Connection with the internal oven token.
+ *
+ * @param message Message object to pass errors to the calling method.
+ * @param thingUID Thing UID for logging purposes
+ * @return true if no error occurred, false otherwise.
+ */
+ public boolean refreshOvenConnection(Helper message, String thingUID) {
+ if (config.hostIP == null || config.hostPIN == null) {
+ message.setStatusDescription("Error in configuration. Please recreate Thing.");
+ return false;
+ }
+ HaasSohnpelletstoveJsonDataDTO result = null;
+ boolean resultOk = false;
+ String error = "", errorDetail = "", statusDescr = "";
+ String urlStr = "http://" + config.hostIP + "/status.cgi";
+
+ String response = null;
+ try {
+ response = HttpUtil.executeUrl("GET", urlStr, 10000);
+ logger.debug("OvenData = {}", response);
+ result = gson.fromJson(response, HaasSohnpelletstoveJsonDataDTO.class);
+ resultOk = true;
+ } catch (IOException e) {
+ logger.debug("Error processiong Get request {}", urlStr);
+ statusDescr = "Timeout error with" + config.hostIP
+ + ". Cannot find service on give IP. Please verify the IP-Address!";
+ errorDetail = e.getMessage();
+ resultOk = false;
+ } catch (Exception e) {
+ logger.debug("Unknwon Error: {}", e.getMessage());
+ errorDetail = e.getMessage();
+ resultOk = false;
+ }
+ if (resultOk) {
+ ovenData = result;
+ xhspin = getValidXHSPIN(ovenData);
+ } else {
+ logger.debug("Setting thing '{}' to OFFLINE: Error '{}': {}", thingUID, error, errorDetail);
+ ovenData = new HaasSohnpelletstoveJsonDataDTO();
+ }
+ message.setStatusDescription(statusDescr);
+ return resultOk;
+ }
+
+ /**
+ * Gets the status of the oven
+ *
+ * @return true if success or false in case of error
+ */
+ public boolean updateOvenData(@Nullable String postData, Helper helper, String thingUID) {
+ String statusDescr = "";
+ boolean resultOk = false;
+ String error = "", errorDetail = "";
+ if (config.hostIP == null || config.hostPIN == null) {
+ return false;
+ }
+ String urlStr = "http://" + config.hostIP + "/status.cgi";
+
+ // Run the HTTP POST request and get the JSON response from Oven
+ String response = null;
+
+ Properties httpHeader = new Properties();
+
+ if (postData != null) {
+ try {
+ InputStream targetStream = new ByteArrayInputStream(postData.getBytes("UTF-8"));
+ refreshOvenConnection(helper, thingUID);
+ httpHeader = createHeader(postData);
+ response = HttpUtil.executeUrl("POST", urlStr, httpHeader, targetStream, "application/json", 10000);
+ resultOk = true;
+ logger.debug("Execute POST request with content to {} with header: {}", urlStr, httpHeader.toString());
+ } catch (UnsupportedEncodingException e1) {
+ logger.debug("Wrong encoding found. Only UTF-8 is supported.");
+ statusDescr = "Encoding of oven is not supported. Only UTF-8 is supported.";
+ resultOk = false;
+ } catch (IOException e) {
+ logger.debug("Error processiong POST request {}", urlStr);
+ statusDescr = "Cannot execute command on Stove. Please verify connection and Thing Status";
+ resultOk = false;
+ }
+ } else {
+ try {
+ refreshOvenConnection(helper, thingUID);
+ httpHeader = createHeader(null);
+ response = HttpUtil.executeUrl("POST", urlStr, httpHeader, null, "", 10000);
+ resultOk = true;
+ logger.debug("Execute POST request to {} with header: {}", urlStr, httpHeader.toString());
+ } catch (IOException e) {
+ logger.debug("Error processiong POST request {}", e.getMessage());
+ String message = e.getMessage();
+ if (message != null && message.contains("Authentication challenge without WWW-Authenticate ")) {
+ statusDescr = "Cannot connect to stove. Given PIN: " + config.hostPIN + " is incorrect!";
+ }
+ resultOk = false;
+ }
+ }
+ if (resultOk) {
+ logger.debug("OvenData = {}", response);
+ ovenData = gson.fromJson(response, HaasSohnpelletstoveJsonDataDTO.class);
+ } else {
+ logger.debug("Setting thing '{}' to OFFLINE: Error '{}': {}", thingUID, error, errorDetail);
+ ovenData = new HaasSohnpelletstoveJsonDataDTO();
+ }
+ helper.setStatusDescription(statusDescr);
+ return resultOk;
+ }
+
+ /**
+ * Creates the header for the Post Request
+ *
+ * @return The created Header Properties
+ * @throws UnsupportedEncodingException
+ */
+ private Properties createHeader(@Nullable String postData) throws UnsupportedEncodingException {
+ Properties httpHeader = new Properties();
+ httpHeader.setProperty("Host", config.hostIP);
+ httpHeader.setProperty("Accept", "*/*");
+ httpHeader.setProperty("Proxy-Connection", "keep-alive");
+ httpHeader.setProperty("X-BACKEND-IP", "https://app.haassohn.com");
+ httpHeader.setProperty("Accept-Language", "de-DE;q=1.0, en-DE;q=0.9");
+ httpHeader.setProperty("Accept-Encoding", "gzip;q=1.0, compress;q=0.5");
+ httpHeader.setProperty("token", "32 bytes");
+ httpHeader.setProperty("Content-Type", "application/json");
+ if (postData != null) {
+ int a = postData.getBytes("UTF-8").length;
+ httpHeader.setProperty(xhspin, Integer.toString(a));
+ }
+ httpHeader.setProperty("User-Agent", "ios");
+ httpHeader.setProperty("Connection", "keep-alive");
+ httpHeader.setProperty("X-HS-PIN", xhspin);
+ return httpHeader;
+ }
+
+ /**
+ * Generate the valid encrypted string to communicate with the oven.
+ *
+ * @param ovenData
+ * @return
+ */
+ private @Nullable String getValidXHSPIN(@Nullable HaasSohnpelletstoveJsonDataDTO ovenData) {
+ if (ovenData != null && config.hostPIN != null) {
+ String nonce = ovenData.getNonce();
+ String hostPIN = config.hostPIN;
+ String ePin = MD5Utils.getMD5String(hostPIN);
+ return MD5Utils.getMD5String(nonce + ePin);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Set the config for service to communicate
+ *
+ * @param config2
+ */
+ public void setConfig(@Nullable HaasSohnpelletstoveConfiguration config2) {
+ if (config2 != null) {
+ this.config = config2;
+ }
+ }
+
+ /**
+ * Returns the actual stored Oven Data
+ *
+ * @return
+ */
+ @Nullable
+ public HaasSohnpelletstoveJsonDataDTO getOvenData() {
+ return this.ovenData;
+ }
+}
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveJsonDataDTO.java b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveJsonDataDTO.java
new file mode 100644
index 00000000000..17c45def8d1
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/HaasSohnpelletstoveJsonDataDTO.java
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.haassohnpelletstove.internal;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link HaasSohnpelletstoveJsonDataDTO} is the Java class used to map the JSON
+ * response to a Oven request.
+ *
+ * @author Christian Feininger - Initial contribution
+ */
+public class HaasSohnpelletstoveJsonDataDTO {
+ metadata meta = new metadata();
+ boolean prg;
+ boolean wprg;
+ String mode = "";
+ @SerializedName("sp_temp")
+ String spTemp = "";
+ @SerializedName("is_temp")
+ String isTemp = "";
+ @SerializedName("ht_char")
+ String htChar = "";
+ @SerializedName("weekprogram")
+ private wprogram[] weekprogram;
+ @SerializedName("error")
+ private err[] error;
+ @SerializedName("eco_mode")
+ boolean ecoMode;
+ boolean pgi;
+ String ignitions = "";
+ @SerializedName("on_time")
+ String onTime = "";
+ String consumption = "";
+ @SerializedName("maintenance_in")
+ String maintenanceIn = "";
+ @SerializedName("cleaning_in")
+ String cleaningIn = "";
+
+ /***
+ * Get the nonce
+ *
+ * @return nonce
+ */
+ public String getNonce() {
+ return this.meta.getNonce();
+ }
+
+ /**
+ * Returns the is Temperature of the Oven
+ *
+ * @return
+ */
+ public String getisTemp() {
+ return isTemp;
+ }
+
+ public boolean getEcoMode() {
+ return ecoMode;
+ }
+
+ public String getIgnitions() {
+ return ignitions;
+ }
+
+ public String getOnTime() {
+ return onTime;
+ }
+
+ public String getConsumption() {
+ return consumption;
+ }
+
+ public String getMaintenanceIn() {
+ return maintenanceIn;
+ }
+
+ public String getCleaningIn() {
+ return cleaningIn;
+ }
+
+ /***
+ * JSON response
+ *
+ * @return JSON response as object
+ */
+ public HaasSohnpelletstoveJsonDataDTO getResponse() {
+ return this;
+ }
+
+ public class metadata {
+ @SerializedName("sw_version")
+ String swVersion = "";
+ @SerializedName("hw_version")
+ String hwVersion = "";
+ @SerializedName("bootl_version")
+ String bootlVersion = "";
+ @SerializedName("wifi_sw_version")
+ String wifiSWVersion = "";
+ @SerializedName("wifi_bootl_version")
+ String wifiBootlVersion = "";
+ String sn = "";
+ String typ = "";
+ String language = "";
+ String nonce = "";
+ @SerializedName("eco_editable")
+ String ecoEditable = "";
+ String ts = "";
+ String ean = "";
+ boolean rau;
+ @SerializedName("wlan_features")
+ private String[] wlan_features;
+
+ public String getNonce() {
+ return nonce;
+ }
+ }
+
+ public class err {
+ String time = "";
+ String nr = "";
+ }
+
+ public class wprogram {
+ String day = "";
+ String begin = "";
+ String end = "";
+ String temp = "";
+ }
+
+ public String getMode() {
+ return mode;
+ }
+
+ public String getspTemp() {
+ return spTemp;
+ }
+
+ public boolean getPrg() {
+ return prg;
+ }
+}
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/Helper.java b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/Helper.java
new file mode 100644
index 00000000000..5f279dc6c60
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/Helper.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.haassohnpelletstove.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link Helper} is a Helper class to overcome Call by value for a Status Description.
+ *
+ *
+ * @author Christian Feininger - Initial contribution
+ */
+@NonNullByDefault
+public class Helper {
+
+ private String statusDescription = "";
+
+ /***
+ * Gets the Status Description
+ *
+ * @return
+ */
+ public String getStatusDesription() {
+ return statusDescription;
+ }
+
+ /***
+ * Sets the Status Description
+ *
+ * @param status
+ */
+ public void setStatusDescription(@Nullable String status) {
+ if (status != null) {
+ statusDescription = statusDescription + "\n" + status;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/MD5Utils.java b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/MD5Utils.java
new file mode 100644
index 00000000000..e1d1da00998
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/java/org/openhab/binding/haassohnpelletstove/internal/MD5Utils.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.haassohnpelletstove.internal;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link MD5Utils} is responsible for generating the MD5 hash
+ *
+ *
+ * @author Christian Feininger - Initial contribution
+ */
+@NonNullByDefault
+public class MD5Utils {
+
+ private static final Charset UTF_8 = StandardCharsets.UTF_8;
+
+ private static byte[] digest(byte[] input) {
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+ byte[] result = md.digest(input);
+ return result;
+ }
+
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+
+ /***
+ * Returns an encrypted MD5 string
+ *
+ * @param input nonce as input
+ * @return Encrypted String
+ */
+ public static String getMD5String(@Nullable String input) {
+ if (input != null) {
+ byte[] md5InBytes = MD5Utils.digest(input.getBytes(UTF_8));
+ return bytesToHex(md5InBytes);
+ }
+ return "";
+ }
+}
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.haassohnpelletstove/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..8dd3fe21d61
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ Haas and Sohn Pelletstove Binding
+ This binding communicates with Haas and Sohn Pelletstoves through the optional WIFI module. It allows to
+ power the stove on and off and receives different operation information.
+
+
diff --git a/bundles/org.openhab.binding.haassohnpelletstove/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.haassohnpelletstove/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 00000000000..dc0d1a74e98
--- /dev/null
+++ b/bundles/org.openhab.binding.haassohnpelletstove/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+ The binding for Haas and Sohn Pelletstove communicates with a Haas and Sohn Pelletstove through the
+ optional
+ WLAN-Modul. More information can be found here: https://www.haassohn.com/de/ihr-plus/WLAN-Funktion. It allows
+ to power on/off the stove as well as receiving different operation information about the stove.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Please add the IP Address of the WIFI Module of the Haas and Sohn oven here
+ network-address
+
+
+
+ Please add the PIN of your oven here. You can find it in the Menu directly in your oven.
+
+
+
+ How often the Pellet Stove should schedule a refresh after a channel is linked to an item. Temperature
+ data will be refreshed according this set time in seconds. Valid input is 0 - 999.
+
+ true
+ 30
+
+
+
+
+
+ Number:Temperature
+
+ Receives the is temperature of the stove as number:temperature
+
+
+
+
+ String
+
+ Receives the actual mode of the stove as string
+
+
+
+
+ Number:Temperature
+
+ Set the target temperature of the stove as number:temperature
+
+
+
+ Switch
+
+ To turn the stove on/off as switch
+
+
+
+ Switch
+
+ To turn the Eco Mode on/off for the stove as switch
+
+
+
+ Number
+
+ Receives the total amount of ignitions of the stove as string
+
+
+
+
+ Number:Mass
+
+ Provides a pellet forecast when the stove need to be maintained next in kilogram as number:mass
+
+
+
+
+ String
+
+ Provides a time forecast in hours:minutes when the stove need to be cleaned next as String
+
+
+
+
+ Number:Mass
+
+ Provides the information about the total consumption of pellets of the stove as number:mass
+
+
+
+
+ Number
+
+ Provides the information of the operating hours of stove as number
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index bf2fe504d7d..f3395fa01cc 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -129,6 +129,7 @@
org.openhab.binding.gpstracker
org.openhab.binding.gree
org.openhab.binding.groheondus
+ org.openhab.binding.haassohnpelletstove
org.openhab.binding.harmonyhub
org.openhab.binding.haywardomnilogic
org.openhab.binding.hdanywhere