/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
<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>
--- /dev/null
+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
--- /dev/null
+# 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
+}
+```
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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.");
+ }
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+
+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.
+
+
+
+
+
+
--- /dev/null
+<?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>
<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>