diff --git a/CODEOWNERS b/CODEOWNERS
index 7cb21544f80..07394845a67 100755
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -212,6 +212,7 @@
/bundles/org.openhab.binding.meteoalerte/ @clinique
/bundles/org.openhab.binding.meteoblue/ @9037568
/bundles/org.openhab.binding.meteostick/ @cdjackson
+/bundles/org.openhab.binding.mffan/ @mark-brooks-180
/bundles/org.openhab.binding.miele/ @kgoderis @jlaur
/bundles/org.openhab.binding.mielecloud/ @BjoernLange
/bundles/org.openhab.binding.mihome/ @pboos
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index c1e15edf948..a87a21c2000 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1056,6 +1056,11 @@
org.openhab.binding.meteostick
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.mffan
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.miele
diff --git a/bundles/org.openhab.binding.mffan/NOTICE b/bundles/org.openhab.binding.mffan/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/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.mffan/README.md b/bundles/org.openhab.binding.mffan/README.md
new file mode 100644
index 00000000000..6c03d7ff7a5
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/README.md
@@ -0,0 +1,69 @@
+# MfFan Binding
+
+This binding is used to enable communications between openHAB and "Modern Forms" or "WAC Lighting" WIFI connected, smart, ceiling fans.
+
+## Supported Things
+
+The binding currently supports the following thing:
+
+| Thing | ID | |
+|---------------|-------------|----------------------------------------------------------------|
+| mffan | mffan | Smart fans consisting of fan and optional integrated LED light |
+
+## Discovery
+
+Auto discovery is not supported at this time.
+
+## Thing Configuration
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|---------------------------------------|---------|----------|----------|
+| hostname | text | Hostname or IP address of the device | N/A | yes | no |
+| refreshInterval | integer | Interval the device is polled in sec. | 120 | no | yes |
+
+## Channels
+
+| Channel | Type | Read/Write | Description |
+|------------------|------------------------|------------|-------------------------------------|
+| fan-on | Switch | RW | Channel that turns the fan on/off. |
+| fan-speed | String | RW | Controls the fan's rate of rotation.|
+| fan-direction | String | RW | Controls the direction of the fan. |
+| wind-on | Switch | RW | Turn the fan's "wind mode" on/off. |
+| wind-level | String | RW | The amount of wind produced. |
+| light-on | Switch | RW | Turns the light on/off |
+| light-intensity | Number:Dimensionless | RW | Controls the intensity of the light |
+
+
+## Full Example
+
+### Thing Configuration
+
+```java
+mffan:mffan:db0bd2eb4d [label="Greatroom Fan", ipAddress="fan.greatroom.local", pollingPeriod = "120"]
+```
+
+### Item Configuration
+
+```java
+ Switch Greatroom_Fan_Fan { channel="mffan:mffan:db0bd2eb4d:fan-on" }
+ String Greatroom_Fan_Fan_Direction {channel="mffan:mffan:db0bd2eb4d:fan-direction" }
+ String Greatroom_Fan_Fan_Speed {channel="mffan:mffan:db0bd2eb4d:fan-speed" }
+ Switch Greatroom_Fan_Light {channel="mffan:mffan:db0bd2eb4d:light-on" }
+ Dimmer Greatroom_Fan_Light_Intensity {channel="mffan:mffan:db0bd2eb4d:light-intensity" }
+ Switch Greatroom_Fan_Wind {channel="mffan:mffan:db0bd2eb4d:wind-on" }
+ String Greatroom_Fan_Wind_Level {channel="mffan:mffan:db0bd2eb4d:wind-level" }
+```
+
+### Sitemap Configuration
+
+```perl
+Group icon=fan_ceiling label="Fan" item=Greatroom_Fan {
+ Switch icon=switch label="Fan On/Off" item=Greatroom_Fan_Fan
+ Selection label="Fan Speed" item=Greatroom_Fan_Fan_Speed
+ Selection label="Fan Direction" item=Greatroom_Fan_Fan_Direction
+ Switch icon=switch label="Window On/Off" item=Greatroom_Fan_Wind
+ Selection label="Wind Level" item=Greatroom_Fan_Wind_Level
+ Switch icon=switch label="Light On/Off" item=Greatroom_Fan_Light
+ Slider label="Light Intensity" item=Greatroom_Fan_Light_Intensity
+}
+```
diff --git a/bundles/org.openhab.binding.mffan/pom.xml b/bundles/org.openhab.binding.mffan/pom.xml
new file mode 100644
index 00000000000..dc4f607e795
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.2.0-SNAPSHOT
+
+
+ org.openhab.binding.mffan
+
+ openHAB Add-ons :: Bundles :: MfFan Binding
+
+
diff --git a/bundles/org.openhab.binding.mffan/src/main/feature/feature.xml b/bundles/org.openhab.binding.mffan/src/main/feature/feature.xml
new file mode 100644
index 00000000000..a5865ae4851
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/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.mffan/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanBindingConstants.java b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanBindingConstants.java
new file mode 100644
index 00000000000..57bbf3a30b6
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanBindingConstants.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2010-2024 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.mffan.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link MfFanBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Mark Brooks - Initial contribution
+ */
+@NonNullByDefault
+public class MfFanBindingConstants {
+
+ private static final String BINDING_ID = "mffan";
+ private static final String THING_MFFAN_ID = "mffan";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID THING_TYPE_MFFAN = new ThingTypeUID(BINDING_ID, THING_MFFAN_ID);
+
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_MFFAN);
+
+ // List of all Channel ids
+ public static final String CHANNEL_FAN_ON = "fan-on";
+ public static final String CHANNEL_FAN_SPEED = "fan-speed";
+ public static final String CHANNEL_FAN_DIRECTION = "fan-direction";
+ public static final String CHANNEL_WIND_ON = "wind-on";
+ public static final String CHANNEL_WIND_LEVEL = "wind-level";
+ public static final String CHANNEL_LIGHT_ON = "light-on";
+ public static final String CHANNEL_LIGHT_INTENSITY = "light-intensity";
+}
diff --git a/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanConfiguration.java b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanConfiguration.java
new file mode 100644
index 00000000000..ea5e922910e
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanConfiguration.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2010-2024 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.mffan.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link MfFanConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Mark Brooks - Initial contribution
+ */
+@NonNullByDefault
+public class MfFanConfiguration {
+
+ private String ipAddress;
+ private Integer pollingPeriod;
+
+ public MfFanConfiguration() {
+ this.ipAddress = "";
+ this.pollingPeriod = 120;
+ }
+
+ public String getIpAddress() {
+ return this.ipAddress.trim();
+ }
+
+ public void setIpAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ public Integer getPollingPeriod() {
+ return this.pollingPeriod;
+ }
+
+ public void setPollingPeriod(Integer pollingPeriod) {
+ this.pollingPeriod = pollingPeriod;
+ }
+
+ public static boolean validateConfig(@Nullable MfFanConfiguration config) {
+ if (config == null || config.getIpAddress().isBlank()) {
+ return false;
+ }
+ return config.getPollingPeriod() >= 10;
+ }
+}
diff --git a/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanHandlerFactory.java b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanHandlerFactory.java
new file mode 100644
index 00000000000..dcabbf8dd79
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanHandlerFactory.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2024 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.mffan.internal;
+
+import static org.openhab.binding.mffan.internal.MfFanBindingConstants.THING_TYPE_MFFAN;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mffan.internal.handler.MfFanHandler;
+import org.openhab.core.io.net.http.HttpClientFactory;
+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.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link MfFanHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Mark Brooks - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.mffan", service = ThingHandlerFactory.class)
+public class MfFanHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_MFFAN);
+ private HttpClientFactory httpClientFactory;
+
+ @Activate
+ public MfFanHandlerFactory(final @Reference HttpClientFactory httpClientFactory) {
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ @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_MFFAN.equals(thingTypeUID)) {
+ return new MfFanHandler(thing, this.httpClientFactory);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/FanRestApi.java b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/FanRestApi.java
new file mode 100644
index 00000000000..5e269d8abf9
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/FanRestApi.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2010-2024 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.mffan.internal.api;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpHeader;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link FanRestApi} is implements provides access to the smart fan's REST services.
+ *
+ * @author Mark Brooks - Initial contribution
+ */
+@NonNullByDefault
+public class FanRestApi {
+ private final Logger logger = LoggerFactory.getLogger(FanRestApi.class);
+
+ private final String ipAddress;
+ private final String url;
+ private final HttpClient client;
+ private final Gson gson;
+
+ public FanRestApi(String ipAddress, HttpClientFactory httpClientFactory) {
+ this.ipAddress = ipAddress;
+ this.url = String.format("http://%s/mf", this.ipAddress);
+ this.client = httpClientFactory.getCommonHttpClient();
+ this.gson = new Gson();
+ }
+
+ @Nullable
+ public ShadowBufferDto getShadowBuffer() throws RestApiException {
+ return doPost("{\"queryDynamicShadowData\" : 1}");
+ }
+
+ @Nullable
+ public ShadowBufferDto setFanPower(boolean power) throws RestApiException {
+ return doPost(String.format("{\"fanOn\" : %s}", String.valueOf(power)));
+ }
+
+ @Nullable
+ public ShadowBufferDto setFanSpeed(int speed) throws RestApiException {
+ return doPost(String.format("{\"fanSpeed\" : %d}", speed));
+ }
+
+ @Nullable
+ public ShadowBufferDto setFanDirection(ShadowBufferDto.FanDirection direction) throws RestApiException {
+ return doPost(String.format("{\"fanDirection\" : \"%s\"}", direction.name()));
+ }
+
+ @Nullable
+ public ShadowBufferDto setWindPower(boolean power) throws RestApiException {
+ return doPost(String.format("{\"wind\" : %s}", String.valueOf(power)));
+ }
+
+ @Nullable
+ public ShadowBufferDto setWindSpeed(int speed) throws RestApiException {
+ return doPost(String.format("{\"windSpeed\" : %d}", speed));
+ }
+
+ @Nullable
+ public ShadowBufferDto setLightPower(boolean power) throws RestApiException {
+ return doPost(String.format("{\"lightOn\" : %s}", String.valueOf(power)));
+ }
+
+ @Nullable
+ public ShadowBufferDto setLightIntensity(int intensity) throws RestApiException {
+ return doPost(String.format("{\"lightBrightness\" : %d}", intensity));
+ }
+
+ @Nullable
+ private ShadowBufferDto doPost(String payloadJson) throws RestApiException {
+ try {
+ this.logger.debug("Performing Post: 'URL: {}, Payload: '{}'", this.url, payloadJson);
+ Request postRequest = this.client.POST(this.url);
+ postRequest.timeout(10, TimeUnit.SECONDS);
+ postRequest.header(HttpHeader.ACCEPT, MediaType.APPLICATION_JSON);
+ postRequest.header(HttpHeader.CONTENT_TYPE, MediaType.APPLICATION_JSON);
+ postRequest.content(new StringContentProvider(payloadJson, Charset.forName(StandardCharsets.UTF_8.name())));
+ ContentResponse postResponse = postRequest.send();
+ this.logger.debug("Response status: {}", postResponse.getStatus());
+ if (postResponse.getStatus() == 200) {
+ this.logger.trace("Post Response Content = '{}'", postResponse.getContentAsString());
+ return this.gson.fromJson(postResponse.getContentAsString(), ShadowBufferDto.class);
+ }
+ } catch (JsonSyntaxException | InterruptedException | TimeoutException | ExecutionException e) {
+ this.logger.warn("Exception on post: {}", e.getMessage());
+ throw new RestApiException(e);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/RestApiException.java b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/RestApiException.java
new file mode 100644
index 00000000000..1857bb41b5a
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/RestApiException.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2024 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.mffan.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link RestApiException} is an exception thrown from the REST API.
+ *
+ * @author Mark Brooks - Initial contribution
+ */
+@NonNullByDefault
+public class RestApiException extends Exception {
+ private static final long serialVersionUID = -6340681561578357625L;
+
+ public RestApiException(String message) {
+ super(message);
+ }
+
+ public RestApiException(Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/ShadowBufferDto.java b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/ShadowBufferDto.java
new file mode 100644
index 00000000000..52590e589f4
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/ShadowBufferDto.java
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2010-2024 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.mffan.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.OnOffType;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link ShadowBufferDto} shadow buffer data transport object.
+ *
+ * @author Mark Brooks - Initial contribution
+ */
+@NonNullByDefault
+public class ShadowBufferDto {
+ @Expose
+ private String clientId;
+
+ @Expose
+ private Integer cloudPort;
+
+ @Expose
+ private Boolean lightOn;
+
+ @Expose
+ private Boolean fanOn;
+
+ @Expose
+ private Integer lightBrightness;
+
+ @Expose
+ private Integer fanSpeed;
+
+ @Expose
+ private FanDirection fanDirection;
+
+ @Expose
+ private Boolean wind;
+
+ @Expose
+ private Integer windSpeed;
+
+ @Expose
+ private Boolean rfPairModeActive;
+
+ @Expose
+ private Boolean resetRfPairList;
+
+ @Expose
+ private Boolean factoryReset;
+
+ @Expose
+ private Boolean awayModeEnabled;
+
+ @Expose
+ private Integer fanTimer;
+
+ @Expose
+ private Integer lightTimer;
+
+ @Expose
+ private Boolean decommission;
+
+ @Expose
+ private String schedule;
+
+ @Expose
+ private Boolean adaptiveLearning;
+
+ @Expose
+ private String userData;
+
+ @Expose
+ private String timezone;
+
+ @Expose
+ @SerializedName("FrCodes")
+ private String frCodes;
+
+ @Expose
+ private Boolean cdebug;
+
+ @Expose
+ private Boolean feedbackToneMute;
+
+ public enum FanDirection {
+ forward,
+ reverse
+ }
+
+ public ShadowBufferDto() {
+ super();
+ this.clientId = "";
+ this.cloudPort = 0;
+ this.lightOn = false;
+ this.fanOn = false;
+ this.lightBrightness = 0;
+ this.fanSpeed = 0;
+ this.fanDirection = FanDirection.forward;
+ this.wind = false;
+ this.windSpeed = 0;
+ this.rfPairModeActive = false;
+ this.resetRfPairList = false;
+ this.factoryReset = false;
+ this.awayModeEnabled = false;
+ this.fanTimer = 0;
+ this.lightTimer = 0;
+ this.decommission = false;
+ this.schedule = "";
+ this.adaptiveLearning = false;
+ this.userData = "";
+ this.timezone = "";
+ this.frCodes = "";
+ this.cdebug = false;
+ this.feedbackToneMute = false;
+ }
+
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ public Integer getCloudPort() {
+ return this.cloudPort;
+ }
+
+ public Boolean getLightOn() {
+ return this.lightOn;
+ }
+
+ public Boolean getFanOn() {
+ return this.fanOn;
+ }
+
+ public OnOffType getFanOnAsOnOffType() {
+ return OnOffType.from(this.fanOn);
+ }
+
+ public Integer getLightBrightness() {
+ return this.lightBrightness;
+ }
+
+ public Integer getFanSpeed() {
+ return this.fanSpeed;
+ }
+
+ public FanDirection getFanDirection() {
+ return this.fanDirection;
+ }
+
+ public Boolean getWind() {
+ return this.wind;
+ }
+
+ public Integer getWindSpeed() {
+ return this.windSpeed;
+ }
+
+ public Boolean getRfPairModeActive() {
+ return this.rfPairModeActive;
+ }
+
+ public Boolean getResetRfPairList() {
+ return this.resetRfPairList;
+ }
+
+ public Boolean getFactoryReset() {
+ return this.factoryReset;
+ }
+
+ public Boolean getAwayModeEnabled() {
+ return this.awayModeEnabled;
+ }
+
+ public Integer getFanTimer() {
+ return this.fanTimer;
+ }
+
+ public Integer getLightTimer() {
+ return this.lightTimer;
+ }
+
+ public Boolean getDecommission() {
+ return this.decommission;
+ }
+
+ public String getSchedule() {
+ return this.schedule;
+ }
+
+ public Boolean getAdaptiveLearning() {
+ return this.adaptiveLearning;
+ }
+
+ public String getUserData() {
+ return this.userData;
+ }
+
+ public String getTimezone() {
+ return this.timezone;
+ }
+
+ public String getFrCodes() {
+ return this.frCodes;
+ }
+
+ public Boolean getCdebug() {
+ return this.cdebug;
+ }
+
+ public Boolean getFeedbackToneMute() {
+ return this.feedbackToneMute;
+ }
+}
diff --git a/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/handler/MfFanHandler.java b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/handler/MfFanHandler.java
new file mode 100644
index 00000000000..13478092790
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/handler/MfFanHandler.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright (c) 2010-2024 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.mffan.internal.handler;
+
+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.mffan.internal.MfFanBindingConstants;
+import org.openhab.binding.mffan.internal.MfFanConfiguration;
+import org.openhab.binding.mffan.internal.api.FanRestApi;
+import org.openhab.binding.mffan.internal.api.RestApiException;
+import org.openhab.binding.mffan.internal.api.ShadowBufferDto;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.library.types.DecimalType;
+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.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.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link MfFanHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Mark Brooks - Initial contribution
+ */
+@NonNullByDefault
+public class MfFanHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(MfFanHandler.class);
+
+ @NonNullByDefault({} /* non-null if initialized */)
+ private MfFanConfiguration config;
+
+ @NonNullByDefault({} /* non-null if initialized */)
+ private FanRestApi api;
+
+ @NonNullByDefault({} /* non-null if initialized */)
+ private ScheduledFuture> pollingJob;
+
+ private HttpClientFactory httpClientFactory;
+
+ public MfFanHandler(Thing thing, HttpClientFactory httpClientFactory) {
+ super(thing);
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ @Override
+ public void initialize() {
+ this.logger.debug("Initializing MfFan handler '{}'", getThing().getUID());
+ updateStatus(ThingStatus.UNKNOWN);
+ this.config = getConfigAs(MfFanConfiguration.class);
+ if (!MfFanConfiguration.validateConfig(this.config)) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid configuration detected.");
+ return;
+ }
+ this.api = new FanRestApi(this.config.getIpAddress(), this.httpClientFactory);
+ this.pollingJob = this.scheduler.scheduleWithFixedDelay(() -> getShadowBufferAndUpdate(), 0,
+ this.config.getPollingPeriod(), TimeUnit.SECONDS);
+ this.logger.debug("Polling job scheduled to run every {} sec. for '{}'", this.config.getPollingPeriod(),
+ getThing().getUID());
+ }
+
+ @Override
+ public void dispose() {
+ this.logger.debug("Disposing MF fan handler '{}'", getThing().getUID());
+ ScheduledFuture> job = this.pollingJob;
+ if (job != null) {
+ job.cancel(true);
+ this.pollingJob = null;
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ try {
+ if (command instanceof RefreshType) {
+ update(MfFanHandler.this.api.getShadowBuffer());
+ } else if (channelUID.getId().equals(MfFanBindingConstants.CHANNEL_FAN_ON)) {
+ if (command instanceof OnOffType onOffCommand) {
+ update(MfFanHandler.this.api.setFanPower(onOffCommand == OnOffType.ON));
+ }
+ } else if (channelUID.getId().equals(MfFanBindingConstants.CHANNEL_FAN_SPEED)) {
+ if (command instanceof StringType stringCommand) {
+ update(MfFanHandler.this.api.setFanSpeed(Integer.valueOf(stringCommand.toString())));
+ }
+ } else if (channelUID.getId().equals(MfFanBindingConstants.CHANNEL_FAN_DIRECTION)) {
+ if (command instanceof StringType stringCommand) {
+ update(MfFanHandler.this.api
+ .setFanDirection(ShadowBufferDto.FanDirection.valueOf(stringCommand.toString())));
+ }
+ } else if (channelUID.getId().equals(MfFanBindingConstants.CHANNEL_LIGHT_ON)) {
+ if (command instanceof OnOffType onOffCommand) {
+ update(MfFanHandler.this.api.setLightPower(onOffCommand == OnOffType.ON));
+ }
+ } else if (channelUID.getId().equals(MfFanBindingConstants.CHANNEL_LIGHT_INTENSITY)) {
+ if (command instanceof QuantityType quantityCommand) {
+ update(MfFanHandler.this.api.setLightIntensity(quantityCommand.intValue()));
+ }
+ } else if (channelUID.getId().equals(MfFanBindingConstants.CHANNEL_WIND_ON)) {
+ if (command instanceof OnOffType onOffCommand) {
+ update(MfFanHandler.this.api.setWindPower(onOffCommand == OnOffType.ON));
+ }
+ } else if (channelUID.getId().equals(MfFanBindingConstants.CHANNEL_WIND_LEVEL)) {
+ if (command instanceof StringType stringCommand) {
+ update(MfFanHandler.this.api.setWindSpeed(Integer.valueOf(stringCommand.toString())));
+ }
+ } else {
+ MfFanHandler.this.logger.warn("Skipping command. Unidentified channel id '{}'", channelUID.getId());
+ }
+ } catch (@SuppressWarnings("unused") RestApiException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, String
+ .format("Could not control device at IP address %s", MfFanHandler.this.config.getIpAddress()));
+ }
+ }
+
+ private void getShadowBufferAndUpdate() {
+ try {
+ update(MfFanHandler.this.api.getShadowBuffer());
+ } catch (@SuppressWarnings("unused") RestApiException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, String
+ .format("Could not control device at IP address %s", MfFanHandler.this.config.getIpAddress()));
+ }
+ }
+
+ private synchronized void update(@Nullable ShadowBufferDto dto) {
+ MfFanHandler.this.logger.debug("Updating data '{}'", getThing().getUID());
+ if (dto != null) {
+ updateState(MfFanBindingConstants.CHANNEL_FAN_ON, OnOffType.from(dto.getFanOn().booleanValue()));
+ updateState(MfFanBindingConstants.CHANNEL_FAN_SPEED, StringType.valueOf(String.valueOf(dto.getFanSpeed())));
+ updateState(MfFanBindingConstants.CHANNEL_FAN_DIRECTION, StringType.valueOf(dto.getFanDirection().name()));
+ updateState(MfFanBindingConstants.CHANNEL_WIND_ON, OnOffType.from(dto.getWind().booleanValue()));
+ updateState(MfFanBindingConstants.CHANNEL_WIND_LEVEL,
+ StringType.valueOf(String.valueOf(dto.getWindSpeed())));
+ updateState(MfFanBindingConstants.CHANNEL_LIGHT_ON, OnOffType.from(dto.getLightOn().booleanValue()));
+ updateState(MfFanBindingConstants.CHANNEL_LIGHT_INTENSITY, new DecimalType(dto.getLightBrightness()));
+ updateStatus(ThingStatus.ONLINE);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ "Null shadow buffer returned.");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 00000000000..1ec2d1b9a44
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,16 @@
+
+
+
+ binding
+ Modern Forms Fan Binding
+ This is the binding for "Modern Forms", and "WAC Lighting" smart ceiling fans.
+ local
+ us
+
+
+ manual
+
+
+
diff --git a/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/i18n/mffan.properties b/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/i18n/mffan.properties
new file mode 100644
index 00000000000..0542a6d4654
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/i18n/mffan.properties
@@ -0,0 +1,47 @@
+
+thing-type.mffan.mffan.label = Modern Forms Fan
+thing-type.mffan.mffan.description = Modern Forms and WAC Lighting Smart Ceiling Fans
+
+thing-type.config.mffan.mffan.ipAddress.label = IP or Host
+thing-type.config.mffan.mffan.ipAddress.description = IP address or host name of the fan.
+thing-type.config.mffan.mffan.pollingPeriod.label = Refresh Interval
+thing-type.config.mffan.mffan.pollingPeriod.description = Interval the device is polled in seconds.
+
+channel-type.mffan.fan-on.label = Fan
+channel-type.mffan.fan-on.description = Fan on/off
+
+
+channel-type.mffan.fan-speed.label = Fan Speed
+channel-type.mffan.fan-speed.description = The fan's rotational rate.
+channel-type.mffan.fan-speed.state.option.1 = Speed 1
+channel-type.mffan.fan-speed.state.option.2 = Speed 2
+channel-type.mffan.fan-speed.state.option.3 = Speed 3
+channel-type.mffan.fan-speed.state.option.4 = Speed 4
+channel-type.mffan.fan-speed.state.option.5 = Speed 5
+channel-type.mffan.fan-speed.state.option.6 = Speed 6
+
+channel-type.mffan.fan-direction.label = Fan Direction
+channel-type.mffan.fan-direction.description = The fan's direction of rotation: Forward (Summer), Reverse (Winter).
+channel-type.mffan.fan-direction.state.option.forward = Forward
+channel-type.mffan.fan-direction.state.option.reverse = Reverse
+
+channel-type.mffan.wind-on.label = Wind
+channel-type.mffan.wind-on.description = Wind (sometimes referred to as "Breeze Mode") on/off.
+
+channel-type.mffan.wind-level.label = Wind Level
+channel-type.mffan.wind-level.description = The amount of the wind being produced.
+channel-type.mffan.wind-level.state.option.1 = Level 1
+channel-type.mffan.wind-level.state.option.2 = Level 2
+channel-type.mffan.wind-level.state.option.3 = Level 3
+
+channel-type.mffan.light-on.label = Light
+channel-type.mffan.light-on.description = Light on/off.
+
+channel-type.mffan.light-intensity.label = Light Intensity
+channel-type.mffan.light-intensity.description = The light intensity.
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/thing/mffan.xml b/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/thing/mffan.xml
new file mode 100644
index 00000000000..365f2747ccd
--- /dev/null
+++ b/bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/thing/mffan.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+ Modern Forms and WAC Lighting Smart Ceiling Fans
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ IP address or host name of the fan.
+
+
+
+
+ Interval the device is polled in seconds.
+ 120
+ true
+
+
+
+
+
+ Switch
+
+ Fan on/off.
+
+
+ String
+
+ The fan's rotational rate.
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ The fan's direction of rotation: Forward (Summer), Reverse (Winter).
+
+
+
+
+
+
+
+
+ Switch
+
+ Wind (sometimes referred to as "Breeze Mode") on/off.
+
+
+ String
+
+ The amount of the wind being produced.
+
+
+
+
+
+
+
+
+
+ Switch
+
+ Light on/off.
+
+
+ Number:Dimensionless
+
+ The light intensity.
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index c0c84ea98b8..026c4569ac0 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -246,6 +246,7 @@
org.openhab.binding.meteoalerte
org.openhab.binding.meteoblue
org.openhab.binding.meteostick
+ org.openhab.binding.mffan
org.openhab.binding.miele
org.openhab.binding.mielecloud
org.openhab.binding.mihome