diff --git a/CODEOWNERS b/CODEOWNERS
index 4a69d5e5764..0eac39f7f38 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -38,6 +38,7 @@
/bundles/org.openhab.binding.boschindego/ @jofleck
/bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
+/bundles/org.openhab.binding.broadlinkthermostat/ @flo_02_mu
/bundles/org.openhab.binding.bsblan/ @hypetsch
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
/bundles/org.openhab.binding.buienradar/ @gedejong
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 8569f1c6e39..2190e4f6ad7 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -176,6 +176,11 @@
org.openhab.binding.bosesoundtouch
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.broadlinkthermostat
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.bsblan
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/NOTICE b/bundles/org.openhab.binding.broadlinkthermostat/NOTICE
new file mode 100644
index 00000000000..33ded0d06e5
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/NOTICE
@@ -0,0 +1,20 @@
+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
+
+== Third-party Content
+
+broadlink-java-api
+* License: MIT License
+* Project: https://github.com/mob41/broadlink-java-api
+* Source: https://github.com/mob41/broadlink-java-api
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/README.md b/bundles/org.openhab.binding.broadlinkthermostat/README.md
new file mode 100644
index 00000000000..6935e4e9c5d
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/README.md
@@ -0,0 +1,67 @@
+# Broadlink Thermostat Binding
+
+The binding integrates devices based on Broadlinkthermostat controllers.
+As the binding uses the [broadlink-java-api](https://github.com/mob41/broadlink-java-api), theoretically all devices supported by the api can be integrated with this binding.
+
+## Supported Things
+
+*Note:* So far only the Floureon Thermostat has been tested! The other things are "best guess" implementations.
+
+| Things | Description | Thing Type |
+|-------------------------|---------------------------------------------------------------|----------------------|
+| Floureon Thermostat | Broadlinkthermostat based Thermostat sold with the branding Floureon | floureonthermostat |
+| Hysen Thermostat | Broadlinkthermostat based Thermostat sold with the branding Hysen | hysenthermostat |
+
+## Discovery
+
+Broadlinkthermostat devices are discovered on the network by sending a specific broadcast message.
+Authentication is automatically sent after creating the thing.
+
+## Thing Configuration
+
+Two parameter are required for creating things:
+
+- `host`: The hostname or IP address of the device.
+- `mac` : The network MAC of the device.
+
+The autodiscovery process finds both parts automatically.
+
+## Channels
+
+### Floureon-/Hysenthermostat
+
+| Channel Type ID | Item Type | Description |
+|-------------------------------|--------------------|----------------------------------------------------------------------|
+| power | Switch | Switch display on/off and enable/disables heating |
+| mode | String | Current mode of the thermostat (`auto` or `manual`) |
+| sensor | String | The sensor (`internal`/`external`) used for triggering the thermostat|
+| roomtemperature | Number:Temperature | Room temperature, measured directly at the device |
+| roomtemperatureexternalsensor | Number:Temperature | Room temperature, measured by an external sensor |
+| active | Switch | Show if thermostat is currently actively heating |
+| setpoint | Number:Temperature | Temperature setpoint that open/close valve |
+| temperatureoffset | Number:Temperature | Manual temperature adjustment |
+| remotelock | Switch | Locks the device to only allow remote actions |
+| time | DateTime | The time and day of week of the device |
+
+## Full Example
+
+demo.things:
+
+```
+Thing broadlinkthermostat:floureonthermostat:bathroomthermostat "Bathroom Thermostat" [ host="192.168.0.23", mac="00:10:FA:6E:38:4A"]
+```
+
+demo.items:
+
+```
+Number:Temperature Bathroom_Thermostat_Temperature "Room temperature [%.1f %unit%]" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:roomtemperature"}
+Number:Temperature Bathroom_Thermostat_Temperature_Ext "Room temperature (ext) [%.1f %unit%]" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:roomtemperature"}
+Number:Temperature Bathroom_Thermostat_Setpoint "Setpoint [%.1f %unit%]" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:setpoint"}
+Switch Bathroom_Thermostat_Power "Power" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:power"}
+Switch Bathroom_Thermostat_Active "Active" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:active"}
+String Bathroom_Thermostat_Mode "Mode" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:mode"}
+String Bathroom_Thermostat_Sensor "Sensor" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:sensor"}
+Switch Bathroom_Thermostat_Lock "Lock" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:remotelock"}
+DateTime Bathroom_Thermostat_Time "Time [%1$tm/%1$td %1$tH:%1$tM]" { channel="broadlinkthermostat:floureonthermostat:bathroomthermostat:time"}
+
+```
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/pom.xml b/bundles/org.openhab.binding.broadlinkthermostat/pom.xml
new file mode 100644
index 00000000000..86e7ed1e917
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.broadlinkthermostat
+
+ openHAB Add-ons :: Bundles :: Broadlink Thermostat Binding
+
+
+
+ com.github.mob41.blapi
+ broadlink-java-api
+ 1.0.1
+ compile
+
+
+
+
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/feature/feature.xml b/bundles/org.openhab.binding.broadlinkthermostat/src/main/feature/feature.xml
new file mode 100644
index 00000000000..90d232095cf
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab.tp-jaxb
+ mvn:org.openhab.addons.bundles/org.openhab.binding.broadlinkthermostat/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatBindingConstants.java b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatBindingConstants.java
new file mode 100755
index 00000000000..027e519de11
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatBindingConstants.java
@@ -0,0 +1,55 @@
+/**
+ * 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.broadlinkthermostat.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link BroadlinkThermostatBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Florian Mueller - Initial contribution
+ */
+@NonNullByDefault
+public class BroadlinkThermostatBindingConstants {
+
+ private static final String BINDING_ID = "broadlinkthermostat";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID FLOUREON_THERMOSTAT_THING_TYPE = new ThingTypeUID(BINDING_ID,
+ "floureonthermostat");
+ public static final ThingTypeUID HYSEN_THERMOSTAT_THING_TYPE = new ThingTypeUID(BINDING_ID, "hysenthermostat");
+ public static final ThingTypeUID UNKNOWN_BROADLINKTHERMOSTAT_THING_TYPE = new ThingTypeUID(BINDING_ID,
+ "unknownbroadlinkthermostatdevice");
+
+ // List of all Channel ids
+ public static final String ROOM_TEMPERATURE = "roomtemperature";
+ public static final String ROOM_TEMPERATURE_EXTERNAL_SENSOR = "roomtemperatureexternalsensor";
+ public static final String SETPOINT = "setpoint";
+ public static final String POWER = "power";
+ public static final String MODE = "mode";
+ public static final String SENSOR = "sensor";
+ public static final String TEMPERATURE_OFFSET = "temperatureoffset";
+ public static final String ACTIVE = "active";
+ public static final String REMOTE_LOCK = "remotelock";
+ public static final String TIME = "time";
+
+ // Config properties
+ public static final String HOST = "host";
+ public static final String DESCRIPTION = "description";
+
+ public static final String MODE_AUTO = "auto";
+ public static final String SENSOR_INTERNAL = "internal";
+ public static final String SENSOR_EXTERNAL = "external";
+}
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatConfig.java b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatConfig.java
new file mode 100644
index 00000000000..0c707aa28c7
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatConfig.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.broadlinkthermostat.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link BroadlinkThermostatConfig} class holds the configuration properties of the thing.
+ *
+ * @author Florian Mueller - Initial contribution
+ */
+
+@NonNullByDefault
+public class BroadlinkThermostatConfig {
+ private String host;
+ private String macAddress;
+
+ public BroadlinkThermostatConfig() {
+ this.host = "0.0.0.0";
+ this.macAddress = "00:00:00:00";
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public String getMacAddress() {
+ return macAddress;
+ }
+
+ public void setMacAddress(String macAddress) {
+ this.macAddress = macAddress;
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatHandlerFactory.java b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatHandlerFactory.java
new file mode 100755
index 00000000000..34efd4d8eaf
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/BroadlinkThermostatHandlerFactory.java
@@ -0,0 +1,55 @@
+/**
+ * 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.broadlinkthermostat.internal;
+
+import static org.openhab.binding.broadlinkthermostat.internal.BroadlinkThermostatBindingConstants.*;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.broadlinkthermostat.internal.handler.FloureonThermostatHandler;
+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 BroadlinkThermostatHandlerFactory} is responsible for creating things and thing handlers.
+ *
+ * @author Florian Mueller - Initial contribution
+ */
+@Component(configurationPid = "binding.broadlinkthermostat", service = ThingHandlerFactory.class)
+@NonNullByDefault
+public class BroadlinkThermostatHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES = Set.of(FLOUREON_THERMOSTAT_THING_TYPE,
+ HYSEN_THERMOSTAT_THING_TYPE, UNKNOWN_BROADLINKTHERMOSTAT_THING_TYPE);
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (FLOUREON_THERMOSTAT_THING_TYPE.equals(thingTypeUID) || HYSEN_THERMOSTAT_THING_TYPE.equals(thingTypeUID)) {
+ return new FloureonThermostatHandler(thing);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/discovery/BroadlinkThermostatDiscoveryService.java b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/discovery/BroadlinkThermostatDiscoveryService.java
new file mode 100755
index 00000000000..1cd396212b7
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/discovery/BroadlinkThermostatDiscoveryService.java
@@ -0,0 +1,194 @@
+/**
+ * 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.broadlinkthermostat.internal.discovery;
+
+import static org.openhab.binding.broadlinkthermostat.internal.BroadlinkThermostatBindingConstants.*;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Map;
+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.binding.broadlinkthermostat.internal.BroadlinkThermostatBindingConstants;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResult;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.net.NetworkAddressService;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.mob41.blapi.BLDevice;
+
+/**
+ * The {@link BroadlinkThermostatDiscoveryService} is responsible for discovering Broadlinkthermostat devices through
+ * Broadcast.
+ *
+ * @author Florian Mueller - Initial contribution
+ */
+@Component(service = DiscoveryService.class, configurationPid = "discovery.broadlinkthermostat")
+@NonNullByDefault
+public class BroadlinkThermostatDiscoveryService extends AbstractDiscoveryService {
+
+ private final Logger logger = LoggerFactory.getLogger(BroadlinkThermostatDiscoveryService.class);
+
+ private final NetworkAddressService networkAddressService;
+
+ private static final Set DISCOVERABLE_THING_TYPES_UIDS = Set.of(FLOUREON_THERMOSTAT_THING_TYPE,
+ UNKNOWN_BROADLINKTHERMOSTAT_THING_TYPE);
+ private static final int DISCOVERY_TIMEOUT_SECONDS = 30;
+ private @Nullable ScheduledFuture> backgroundDiscoveryFuture;
+
+ @Activate
+ public BroadlinkThermostatDiscoveryService(@Reference NetworkAddressService networkAddressService) {
+ super(DISCOVERABLE_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_SECONDS);
+ this.networkAddressService = networkAddressService;
+ }
+
+ private void createScanner() {
+
+ long timestampOfLastScan = getTimestampOfLastScan();
+ BLDevice[] blDevices = new BLDevice[0];
+ try {
+ @Nullable
+ InetAddress sourceAddress = getIpAddress();
+ if (sourceAddress != null) {
+ logger.debug("Using source address {} for sending out broadcast request.", sourceAddress);
+ blDevices = BLDevice.discoverDevices(sourceAddress, 0, DISCOVERY_TIMEOUT_SECONDS * 1000);
+ } else {
+ blDevices = BLDevice.discoverDevices(DISCOVERY_TIMEOUT_SECONDS * 1000);
+ }
+ } catch (IOException e) {
+ logger.debug("Error while trying to discover broadlinkthermostat devices: {}", e.getMessage());
+ }
+ logger.debug("Discovery service found {} broadlinkthermostat devices.", blDevices.length);
+
+ for (BLDevice dev : blDevices) {
+ logger.debug("Broadlinkthermostat device {} of type {} with Host {} and MAC {}", dev.getDeviceDescription(),
+ Integer.toHexString(dev.getDeviceType()), dev.getHost(), dev.getMac());
+
+ ThingUID thingUID;
+ String id = dev.getHost().replaceAll("\\.", "-");
+ logger.debug("Device ID with IP address replacement: {}", id);
+ try {
+ id = getHostnameWithoutDomain(InetAddress.getByName(dev.getHost()).getHostName());
+ logger.debug("Device ID with DNS name: {}", id);
+ } catch (UnknownHostException e) {
+ logger.debug("Discovered device with IP {} does not have a DNS name, using IP as thing UID.",
+ dev.getHost());
+ }
+
+ switch (dev.getDeviceDescription()) {
+ case "Floureon Thermostat":
+ thingUID = new ThingUID(FLOUREON_THERMOSTAT_THING_TYPE, id);
+ break;
+ case "Hysen Thermostat":
+ thingUID = new ThingUID(HYSEN_THERMOSTAT_THING_TYPE, id);
+ break;
+ default:
+ thingUID = new ThingUID(UNKNOWN_BROADLINKTHERMOSTAT_THING_TYPE, id);
+ }
+
+ Map properties = new HashMap<>();
+ properties.put(BroadlinkThermostatBindingConstants.HOST, dev.getHost());
+ properties.put(Thing.PROPERTY_MAC_ADDRESS, dev.getMac().getMacString());
+ properties.put(BroadlinkThermostatBindingConstants.DESCRIPTION, dev.getDeviceDescription());
+
+ logger.debug("Property map: {}", properties);
+
+ DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+ .withLabel(dev.getDeviceDescription() + " (" + id + ")")
+ .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).build();
+
+ thingDiscovered(discoveryResult);
+ }
+ removeOlderResults(timestampOfLastScan);
+ }
+
+ @Override
+ protected void startScan() {
+ scheduler.execute(this::createScanner);
+ }
+
+ @Override
+ protected void startBackgroundDiscovery() {
+ logger.trace("Starting background scan for Broadlinkthermostat devices");
+ ScheduledFuture> currentBackgroundDiscoveryFuture = backgroundDiscoveryFuture;
+ if (currentBackgroundDiscoveryFuture != null) {
+ currentBackgroundDiscoveryFuture.cancel(true);
+ }
+ backgroundDiscoveryFuture = scheduler.scheduleWithFixedDelay(this::createScanner, 0, 60, TimeUnit.SECONDS);
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ logger.trace("Stopping background scan for Broadlinkthermostat devices");
+ @Nullable
+ ScheduledFuture> backgroundDiscoveryFuture = this.backgroundDiscoveryFuture;
+ if (backgroundDiscoveryFuture != null && !backgroundDiscoveryFuture.isCancelled()) {
+ if (backgroundDiscoveryFuture.cancel(true)) {
+ this.backgroundDiscoveryFuture = null;
+ }
+ }
+ stopScan();
+ }
+
+ private @Nullable InetAddress getIpAddress() {
+ return getIpFromNetworkAddressService().orElse(null);
+ }
+
+ /**
+ * Uses openHAB's NetworkAddressService to determine the local primary network interface.
+ *
+ * @return local ip or empty
if configured primary IP is not set or could not be parsed.
+ */
+ private Optional getIpFromNetworkAddressService() {
+ String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
+ if (ipAddress == null) {
+ logger.warn("No network interface could be found.");
+ return Optional.empty();
+ }
+ try {
+ return Optional.of(InetAddress.getByName(ipAddress));
+ } catch (UnknownHostException e) {
+ logger.warn("Configured primary IP cannot be parsed: {} Details: {}", ipAddress, e.getMessage());
+ return Optional.empty();
+ }
+ }
+
+ private String getHostnameWithoutDomain(String hostname) {
+ String broadlinkthermostatRegex = "BroadLink-OEM[-A-Za-z0-9]{12}.*";
+ if (hostname.matches(broadlinkthermostatRegex)) {
+ String[] dotSeparatedString = hostname.split("\\.");
+ logger.debug("Found original broadlink DNS name {}, removing domain", hostname);
+ return dotSeparatedString[0].replaceAll("\\.", "-");
+ } else {
+ logger.debug("DNS name does not match original broadlink name: {}, using it without modification. ",
+ hostname);
+ return hostname.replaceAll("\\.", "-");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/handler/BroadlinkThermostatHandler.java b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/handler/BroadlinkThermostatHandler.java
new file mode 100755
index 00000000000..4890f1ef1e9
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/handler/BroadlinkThermostatHandler.java
@@ -0,0 +1,90 @@
+/**
+ * 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.broadlinkthermostat.internal.handler;
+
+import java.io.IOException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.broadlinkthermostat.internal.BroadlinkThermostatConfig;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.mob41.blapi.BLDevice;
+
+/**
+ * The {@link BroadlinkThermostatHandler} is the device handler class for a broadlinkthermostat device.
+ *
+ * @author Florian Mueller - Initial contribution
+ */
+@NonNullByDefault
+public abstract class BroadlinkThermostatHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(BroadlinkThermostatHandler.class);
+
+ @Nullable
+ BLDevice blDevice;
+ private @Nullable ScheduledFuture> scanJob;
+ @Nullable
+ String host;
+ @Nullable
+ String macAddress;
+
+ /**
+ * Creates a new instance of this class for the {@link Thing}.
+ *
+ * @param thing the thing that should be handled, not null
+ */
+ BroadlinkThermostatHandler(Thing thing) {
+ super(thing);
+ }
+
+ void authenticate(boolean reauth) {
+ logger.debug("Authenticating with broadlinkthermostat device {}...", thing.getLabel());
+ try {
+ BLDevice blDevice = this.blDevice;
+ if (blDevice != null && blDevice.auth(reauth)) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Error while authenticating broadlinkthermostat device " + thing.getLabel() + ":" + e.getMessage());
+ }
+ }
+
+ @Override
+ public void initialize() {
+ BroadlinkThermostatConfig config = getConfigAs(BroadlinkThermostatConfig.class);
+ host = config.getHost();
+ macAddress = config.getMacAddress();
+
+ // schedule a new scan every minute
+ scanJob = scheduler.scheduleWithFixedDelay(this::refreshData, 0, 1, TimeUnit.MINUTES);
+ }
+
+ protected abstract void refreshData();
+
+ @Override
+ public void dispose() {
+ ScheduledFuture> currentScanJob = scanJob;
+ if (currentScanJob != null) {
+ currentScanJob.cancel(true);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/handler/FloureonThermostatHandler.java b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/handler/FloureonThermostatHandler.java
new file mode 100755
index 00000000000..7f615997d01
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/java/org/openhab/binding/broadlinkthermostat/internal/handler/FloureonThermostatHandler.java
@@ -0,0 +1,279 @@
+/**
+ * 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.broadlinkthermostat.internal.handler;
+
+import static org.openhab.binding.broadlinkthermostat.internal.BroadlinkThermostatBindingConstants.*;
+
+import java.io.IOException;
+import java.time.LocalTime;
+import java.time.ZonedDateTime;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.cache.ExpiringCache;
+import org.openhab.core.library.types.DateTimeType;
+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.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.mob41.blapi.FloureonDevice;
+import com.github.mob41.blapi.dev.hysen.AdvancedStatusInfo;
+import com.github.mob41.blapi.dev.hysen.BaseStatusInfo;
+import com.github.mob41.blapi.dev.hysen.SensorControl;
+import com.github.mob41.blapi.mac.Mac;
+import com.github.mob41.blapi.pkt.cmd.hysen.SetTimeCommand;
+
+/**
+ * The {@link FloureonThermostatHandler} is responsible for handling thermostats labeled as Floureon Thermostat.
+ *
+ * @author Florian Mueller - Initial contribution
+ */
+@NonNullByDefault
+public class FloureonThermostatHandler extends BroadlinkThermostatHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(FloureonThermostatHandler.class);
+ private @Nullable FloureonDevice floureonDevice;
+
+ private static final long CACHE_EXPIRY = TimeUnit.SECONDS.toSeconds(3);
+ private final ExpiringCache advancedStatusInfoExpiringCache = new ExpiringCache<>(CACHE_EXPIRY,
+ this::refreshAdvancedStatus);
+
+ /**
+ * Creates a new instance of this class for the {@link FloureonThermostatHandler}.
+ *
+ * @param thing the thing that should be handled, not null
+ */
+ public FloureonThermostatHandler(Thing thing) {
+ super(thing);
+ }
+
+ /**
+ * Initializes a new instance of a {@link FloureonThermostatHandler}.
+ */
+ @Override
+ public void initialize() {
+ super.initialize();
+ if (host != null && macAddress != null) {
+ try {
+ blDevice = new FloureonDevice(host, new Mac(macAddress));
+ this.floureonDevice = (FloureonDevice) blDevice;
+ updateStatus(ThingStatus.ONLINE);
+ } catch (IOException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Could not find broadlinkthermostat device at host" + host + "with MAC+" + macAddress + ": "
+ + e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ logger.debug("Command: {}", command.toFullString());
+ authenticate(false);
+
+ if (command == RefreshType.REFRESH) {
+ refreshData();
+ return;
+ }
+
+ switch (channelUID.getIdWithoutGroup()) {
+ case SETPOINT:
+ handleSetpointCommand(channelUID, command);
+ break;
+ case POWER:
+ handlePowerCommand(channelUID, command);
+ break;
+ case MODE:
+ handleModeCommand(channelUID, command);
+ break;
+ case SENSOR:
+ handleSensorCommand(channelUID, command);
+ break;
+ case REMOTE_LOCK:
+ handleRemoteLockCommand(channelUID, command);
+ break;
+ case TIME:
+ handleSetTimeCommand(channelUID, command);
+ break;
+ default:
+ logger.warn("Channel {} does not support command {}", channelUID, command);
+ }
+ }
+
+ private void handlePowerCommand(ChannelUID channelUID, Command command) {
+ FloureonDevice floureonDevice = this.floureonDevice;
+ if (command instanceof OnOffType && floureonDevice != null) {
+ try {
+ floureonDevice.setPower(command == OnOffType.ON);
+ } catch (Exception e) {
+ logger.warn("Error while setting power of {} to {}: {}", thing.getUID(), command, e.getMessage());
+ }
+ } else {
+ logger.warn("Channel {} does not support command {}", channelUID, command);
+ }
+ }
+
+ private void handleModeCommand(ChannelUID channelUID, Command command) {
+ FloureonDevice floureonDevice = this.floureonDevice;
+ if (command instanceof StringType && floureonDevice != null) {
+ try {
+ if (MODE_AUTO.equals(command.toFullString())) {
+ floureonDevice.switchToAuto();
+ } else {
+ floureonDevice.switchToManual();
+ }
+ } catch (Exception e) {
+ logger.warn("Error while setting power off {} to {}: {}", thing.getUID(), command, e.getMessage());
+ }
+ } else {
+ logger.warn("Channel {} does not support command {}", channelUID, command);
+ }
+ }
+
+ private void handleSetpointCommand(ChannelUID channelUID, Command command) {
+ FloureonDevice floureonDevice = this.floureonDevice;
+ if (command instanceof QuantityType && floureonDevice != null) {
+ try {
+ QuantityType> temperatureQuantityType = ((QuantityType>) command).toUnit(SIUnits.CELSIUS);
+ if (temperatureQuantityType != null) {
+ floureonDevice.setThermostatTemp(temperatureQuantityType.doubleValue());
+ } else {
+ logger.warn("Could not convert {} to °C", command);
+ }
+ } catch (Exception e) {
+ logger.warn("Error while setting setpoint of {} to {}: {}", thing.getUID(), command, e.getMessage());
+ }
+
+ } else {
+ logger.warn("Channel {} does not support command {}", channelUID, command);
+ }
+ }
+
+ private void handleSensorCommand(ChannelUID channelUID, Command command) {
+ FloureonDevice floureonDevice = this.floureonDevice;
+ if (command instanceof StringType && floureonDevice != null) {
+ try {
+ BaseStatusInfo statusInfo = floureonDevice.getBasicStatus();
+ if (SENSOR_INTERNAL.equals(command.toFullString())) {
+ floureonDevice.setMode(statusInfo.getAutoMode(), statusInfo.getLoopMode(), SensorControl.INTERNAL);
+ } else if (SENSOR_EXTERNAL.equals(command.toFullString())) {
+ floureonDevice.setMode(statusInfo.getAutoMode(), statusInfo.getLoopMode(), SensorControl.EXTERNAL);
+ } else {
+ floureonDevice.setMode(statusInfo.getAutoMode(), statusInfo.getLoopMode(),
+ SensorControl.INTERNAL_TEMP_EXTERNAL_LIMIT);
+ }
+ } catch (Exception e) {
+ logger.warn("Error while trying to set sensor mode {}: {}", command, e.getMessage());
+ }
+ } else {
+ logger.warn("Channel {} does not support command {}", channelUID, command);
+ }
+ }
+
+ private void handleRemoteLockCommand(ChannelUID channelUID, Command command) {
+ FloureonDevice floureonDevice = this.floureonDevice;
+ if (command instanceof OnOffType && floureonDevice != null) {
+ try {
+ floureonDevice.setLock(command == OnOffType.ON);
+ } catch (Exception e) {
+ logger.warn("Error while setting remote lock of {} to {}: {}", thing.getUID(), command, e.getMessage());
+ }
+ } else {
+ logger.warn("Channel {} does not support command {}", channelUID, command);
+ }
+ }
+
+ private void handleSetTimeCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof DateTimeType) {
+ ZonedDateTime zonedDateTime = ((DateTimeType) command).getZonedDateTime();
+ try {
+ new SetTimeCommand(tob(zonedDateTime.getHour()), tob(zonedDateTime.getMinute()),
+ tob(zonedDateTime.getSecond()), tob(zonedDateTime.getDayOfWeek().getValue()))
+ .execute(floureonDevice);
+ } catch (Exception e) {
+ logger.warn("Error while setting time of {} to {}: {}", thing.getUID(), command, e.getMessage());
+ }
+ } else {
+ logger.warn("Channel {} does not support command {}", channelUID, command);
+ }
+ }
+
+ @Nullable
+ private AdvancedStatusInfo refreshAdvancedStatus() {
+ if (ThingStatus.ONLINE != thing.getStatus()) {
+ return null;
+ }
+
+ FloureonDevice floureonDevice = this.floureonDevice;
+ if (floureonDevice != null) {
+ try {
+ AdvancedStatusInfo advancedStatusInfo = floureonDevice.getAdvancedStatus();
+ if (advancedStatusInfo == null) {
+ logger.warn("Device {} did not return any data. Trying to reauthenticate...", thing.getUID());
+ authenticate(true);
+ advancedStatusInfo = floureonDevice.getAdvancedStatus();
+ }
+ if (advancedStatusInfo == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Device not responding.");
+ return null;
+ }
+ return advancedStatusInfo;
+ } catch (Exception e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "Error while retrieving data for " + thing.getUID() + ": " + e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void refreshData() {
+
+ AdvancedStatusInfo advancedStatusInfo = advancedStatusInfoExpiringCache.getValue();
+ if (advancedStatusInfo == null) {
+ return;
+ }
+ logger.trace("Retrieved data from device {}: {}", thing.getUID(), advancedStatusInfo);
+ updateState(ROOM_TEMPERATURE, new QuantityType<>(advancedStatusInfo.getRoomTemp(), SIUnits.CELSIUS));
+ updateState(ROOM_TEMPERATURE_EXTERNAL_SENSOR,
+ new QuantityType<>(advancedStatusInfo.getExternalTemp(), SIUnits.CELSIUS));
+ updateState(SETPOINT, new QuantityType<>(advancedStatusInfo.getThermostatTemp(), SIUnits.CELSIUS));
+ updateState(POWER, OnOffType.from(advancedStatusInfo.getPower()));
+ updateState(MODE, StringType.valueOf(advancedStatusInfo.getAutoMode() ? "auto" : "manual"));
+ updateState(SENSOR, StringType.valueOf(advancedStatusInfo.getSensorControl().name()));
+ updateState(TEMPERATURE_OFFSET, new QuantityType<>(advancedStatusInfo.getDif(), SIUnits.CELSIUS));
+ updateState(ACTIVE, OnOffType.from(advancedStatusInfo.getActive()));
+ updateState(REMOTE_LOCK, OnOffType.from(advancedStatusInfo.getRemoteLock()));
+ updateState(TIME, new DateTimeType(getTimestamp(advancedStatusInfo)));
+ }
+
+ private ZonedDateTime getTimestamp(AdvancedStatusInfo advancedStatusInfo) {
+ ZonedDateTime now = ZonedDateTime.now();
+ return now.with(
+ LocalTime.of(advancedStatusInfo.getHour(), advancedStatusInfo.getMin(), advancedStatusInfo.getSec()));
+ }
+
+ private static byte tob(int in) {
+ return (byte) (in & 0xff);
+ }
+}
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 00000000000..1b07d3ec7f9
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,8 @@
+
+
+
+ Broadlinkthermostat Binding
+ This is the binding for Broadlinkthermostat devices.
+
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/config/config.xml
new file mode 100644
index 00000000000..ae22ca06df3
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/config/config.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Hostname
+ The hostname/IP address the device is bound to, e.g. 192.168.0.2
+ network-address
+
+
+ MAC Address
+ The unique MAC address of the device, e.g. 00:10:FA:6E:38:4A
+
+
+
+
diff --git a/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 00000000000..b5ede01d550
--- /dev/null
+++ b/bundles/org.openhab.binding.broadlinkthermostat/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,136 @@
+
+
+
+
+
+ Floureon Thermostat
+ A heating device thermostat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ host
+
+
+
+
+ Hysen Thermostat
+ A heating device thermostat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ host
+
+
+
+
+
+ Switch
+ Power
+ Switch display on/off and enable/disables heating
+ Switch
+
+
+ String
+ Mode
+ Current mode of the thermostat
+
+
+ auto
+ manual
+
+
+
+
+ String
+ Sensor
+ The sensor (internal/external) used for triggering the thermostat
+ Sensor
+
+
+ internal
+ external
+ internal control temperature; external limit temperature
+
+
+
+
+ Switch
+ Active
+ Shows if thermostat is currently actively heating
+ Switch
+
+
+
+ Number:Temperature
+ Temperature
+ Room temperature, measured directly at the device
+ Temperature
+
+
+
+ Number:Temperature
+ Temperature Ext. Sensor
+ Room temperature, measured by the external sensor
+ Temperature
+
+
+
+ Number:Temperature
+ Setpoint
+ Temperature setpoint that open/close valve
+ Temperature
+
+
+
+ Number:Temperature
+ Temperature Offset
+ Manual temperature adjustment
+ Temperature
+
+
+
+ Number:Temperature
+ Temperature
+ Temperature
+ Temperature
+
+
+
+ Switch
+ Remote Lock
+ Locks the device to only allow remote actions
+ Lock
+
+
+ DateTime
+ Time
+ The time and day of week
+ Time
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 9722fb7f97c..eda37e90ca5 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -69,6 +69,7 @@
org.openhab.binding.boschindego
org.openhab.binding.boschshc
org.openhab.binding.bosesoundtouch
+ org.openhab.binding.broadlinkthermostat
org.openhab.binding.bsblan
org.openhab.binding.bticinosmarther
org.openhab.binding.buienradar