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