[mffan] Initial contribution (#16786)

* Added entry for binding mffan

Signed-off-by: mark-brooks-180 <mark.brooks.180@gmail.com>
This commit is contained in:
mark-brooks-180 2024-06-15 12:40:31 -04:00 committed by GitHub
parent 30fbc999e2
commit 20868ec5d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 979 additions and 0 deletions

View File

@ -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

View File

@ -1056,6 +1056,11 @@
<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>

View File

@ -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

View File

@ -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
}
```

View File

@ -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>

View File

@ -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>

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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.");
}
}
}

View File

@ -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>

View File

@ -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.

View File

@ -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>

View File

@ -246,6 +246,7 @@
<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>