From 20868ec5d86153ed8a01888c6d64a50da21cb5ab Mon Sep 17 00:00:00 2001 From: mark-brooks-180 <37186358+mark-brooks-180@users.noreply.github.com> Date: Sat, 15 Jun 2024 12:40:31 -0400 Subject: [PATCH] [mffan] Initial contribution (#16786) * Added entry for binding mffan Signed-off-by: mark-brooks-180 --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.mffan/NOTICE | 13 + bundles/org.openhab.binding.mffan/README.md | 69 ++++++ bundles/org.openhab.binding.mffan/pom.xml | 17 ++ .../src/main/feature/feature.xml | 9 + .../mffan/internal/MfFanBindingConstants.java | 45 ++++ .../mffan/internal/MfFanConfiguration.java | 56 +++++ .../mffan/internal/MfFanHandlerFactory.java | 64 +++++ .../mffan/internal/api/FanRestApi.java | 119 +++++++++ .../mffan/internal/api/RestApiException.java | 33 +++ .../mffan/internal/api/ShadowBufferDto.java | 225 ++++++++++++++++++ .../mffan/internal/handler/MfFanHandler.java | 162 +++++++++++++ .../src/main/resources/OH-INF/addon/addon.xml | 16 ++ .../resources/OH-INF/i18n/mffan.properties | 47 ++++ .../src/main/resources/OH-INF/thing/mffan.xml | 97 ++++++++ bundles/pom.xml | 1 + 17 files changed, 979 insertions(+) create mode 100644 bundles/org.openhab.binding.mffan/NOTICE create mode 100644 bundles/org.openhab.binding.mffan/README.md create mode 100644 bundles/org.openhab.binding.mffan/pom.xml create mode 100644 bundles/org.openhab.binding.mffan/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanBindingConstants.java create mode 100644 bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanConfiguration.java create mode 100644 bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/MfFanHandlerFactory.java create mode 100644 bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/FanRestApi.java create mode 100644 bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/RestApiException.java create mode 100644 bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/api/ShadowBufferDto.java create mode 100644 bundles/org.openhab.binding.mffan/src/main/java/org/openhab/binding/mffan/internal/handler/MfFanHandler.java create mode 100644 bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/i18n/mffan.properties create mode 100644 bundles/org.openhab.binding.mffan/src/main/resources/OH-INF/thing/mffan.xml 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