]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mffan] Initial contribution (#16786)
authormark-brooks-180 <37186358+mark-brooks-180@users.noreply.github.com>
Sat, 15 Jun 2024 16:40:31 +0000 (12:40 -0400)
committerGitHub <noreply@github.com>
Sat, 15 Jun 2024 16:40:31 +0000 (18:40 +0200)
* Added entry for binding mffan

Signed-off-by: mark-brooks-180 <mark.brooks.180@gmail.com>
17 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.mffan/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.mffan/README.md [new file with mode: 0644]
bundles/org.openhab.binding.mffan/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/FanRestApi.java [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/RestApiException.java [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/ShadowBufferDto.java [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/handler/MfFanHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/addon/addon.xml [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/i18n/mffan.properties [new file with mode: 0644]
bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/thing/mffan.xml [new file with mode: 0644]
bundles/pom.xml

index 7cb21544f805192c6a80bebe1b8fba2a498569f8..07394845a67a44e5ed0fa0e8028846c1b33bd9cc 100755 (executable)
 /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
index c1e15edf9485de923a88760a5f9678df64516f9d..a87a21c20000adf14a8f2794e29c1cdaa8330940 100644 (file)
       <artifactId>org.openhab.binding.meteostick</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.mffan</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.miele</artifactId>
diff --git a/bundles/org.openhab.binding.mffan/NOTICE b/bundles/org.openhab.binding.mffan/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..6c03d7f
--- /dev/null
@@ -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 (file)
index 0000000..dc4f607
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>4.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.mffan</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: MfFan Binding</name>
+
+</project>
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 (file)
index 0000000..a5865ae
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.mffan-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-mffan" description="MfFan Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mffan/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..57bbf3a
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..ea5e922
--- /dev/null
@@ -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 (file)
index 0000000..dcabbf8
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..5e269d8
--- /dev/null
@@ -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 (file)
index 0000000..1857bb4
--- /dev/null
@@ -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 (file)
index 0000000..52590e5
--- /dev/null
@@ -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 (file)
index 0000000..1347809
--- /dev/null
@@ -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 (file)
index 0000000..1ec2d1b
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<addon:addon id="mffan" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
+
+       <type>binding</type>
+       <name>Modern Forms Fan Binding</name>
+       <description>This is the binding for "Modern Forms", and "WAC Lighting" smart ceiling fans.</description>
+       <connection>local</connection>
+       <countries>us</countries>
+       <discovery-methods>
+               <discovery-method>
+                       <service-type>manual</service-type>
+               </discovery-method>
+       </discovery-methods>
+</addon:addon>
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 (file)
index 0000000..0542a6d
--- /dev/null
@@ -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 (file)
index 0000000..365f274
--- /dev/null
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mffan"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="mffan">
+
+               <label>Modern Forms Fan</label>
+               <description>Modern Forms and WAC Lighting Smart Ceiling Fans</description>
+
+               <channels>
+                       <channel id="fan-on" typeId="fan-on"/>
+                       <channel id="fan-speed" typeId="fan-speed"/>
+                       <channel id="fan-direction" typeId="fan-direction"/>
+                       <channel id="wind-on" typeId="wind-on"/>
+                       <channel id="wind-level" typeId="wind-level"/>
+                       <channel id="light-on" typeId="light-on"/>
+                       <channel id="light-intensity" typeId="light-intensity"/>
+               </channels>
+
+               <config-description>
+                       <parameter name="ipAddress" type="text" required="true">
+                               <label>IP or Host</label>
+                               <description>IP address or host name of the fan.</description>
+                       </parameter>
+
+                       <parameter name="pollingPeriod" type="integer" unit="s" min="10">
+                               <label>Refresh Interval</label>
+                               <description>Interval the device is polled in seconds.</description>
+                               <default>120</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-type id="fan-on">
+               <item-type>Switch</item-type>
+               <label>Fan</label>
+               <description>Fan on/off.</description>
+       </channel-type>
+       <channel-type id="fan-speed">
+               <item-type>String</item-type>
+               <label>Fan Speed</label>
+               <description>The fan's rotational rate.</description>
+               <state readOnly="false" pattern="Fan %s">
+                       <options>
+                               <option value="1">Speed 1</option>
+                               <option value="2">Speed 2</option>
+                               <option value="3">Speed 3</option>
+                               <option value="4">Speed 4</option>
+                               <option value="5">Speed 5</option>
+                               <option value="6">Speed 6</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="fan-direction">
+               <item-type>String</item-type>
+               <label>Fan Direction</label>
+               <description>The fan's direction of rotation: Forward (Summer), Reverse (Winter).</description>
+               <state readOnly="false" pattern="Direction %s">
+                       <options>
+                               <option value="forward">Forward</option>
+                               <option value="reverse">Reverse</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="wind-on">
+               <item-type>Switch</item-type>
+               <label>Wind</label>
+               <description>Wind (sometimes referred to as "Breeze Mode") on/off.</description>
+       </channel-type>
+       <channel-type id="wind-level">
+               <item-type>String</item-type>
+               <label>Wind Level</label>
+               <description>The amount of the wind being produced.</description>
+               <state readOnly="false" pattern="Wind %s">
+                       <options>
+                               <option value="1">Level 1</option>
+                               <option value="2">Level 2</option>
+                               <option value="3">Level 3</option>
+                       </options>
+               </state>
+       </channel-type>
+       <channel-type id="light-on">
+               <item-type>Switch</item-type>
+               <label>Light</label>
+               <description>Light on/off.</description>
+       </channel-type>
+       <channel-type id="light-intensity">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Light Intensity</label>
+               <description>The light intensity.</description>
+               <state readOnly="false" min="1" max="100" step="1" pattern="%d %unit%"/>
+       </channel-type>
+
+</thing:thing-descriptions>
index c0c84ea98b8f7a2a394edd327a6feaba99fb4234..026c4569ac08f616821fe1fb5014cf579fc753fa 100644 (file)
     <module>org.openhab.binding.meteoalerte</module>
     <module>org.openhab.binding.meteoblue</module>
     <module>org.openhab.binding.meteostick</module>
+    <module>org.openhab.binding.mffan</module>
     <module>org.openhab.binding.miele</module>
     <module>org.openhab.binding.mielecloud</module>
     <module>org.openhab.binding.mihome</module>