/**
* Copyright (c) 2010-2016, openHAB.org and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.myq.internal;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.myq.MyqBindingProvider;
import org.openhab.core.binding.AbstractBinding;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.TypeParser;
import org.openhab.core.types.UnDefType;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* this class polls the Chamberlain MyQ API and sends updates to the event bus
* of configured items in openHAB
*
* @author Scott Hanson
* @author Dan Cunningham
* @since 1.8.0
*/
public class MyqBinding extends AbstractBinding<MyqBindingProvider> {
private static final Logger logger = LoggerFactory.getLogger(MyqBinding.class);
/**
* The BundleContext. This is only valid when the bundle is ACTIVE. It is
* set in the activate() method and must not be accessed anymore once the
* deactivate() method was called or before activate() was called.
*/
@SuppressWarnings("unused")
private BundleContext bundleContext;
/**
* The myqData. This object stores the connection data and makes API
* requests
*/
private MyqData myqOnlineData = null;
/**
* the refresh interval which is used to poll values from the myq server
* (optional, defaults to 60000ms)
*/
private long refreshInterval = 60000;
/**
* We use our own polling service so we can adjust the polling rate during
* periods of activity when a device has been sent a command or is in motion
*/
private ScheduledExecutorService pollService = Executors.newSingleThreadScheduledExecutor();
/**
* The regular polling task
*/
private ScheduledFuture<?> pollFuture;
/**
* This task will reset the poll interval back to normal after a rapid poll
* cycle
*/
private ScheduledFuture<?> pollResetFuture;
/**
* When polling quickly, how often do we poll
*/
private int rapidRefresh = 2000;
/**
* Cap the time we poll rapidly to not overwhelm the servers with api
* requests.
*/
private static int MAX_RAPID_REFRESH = 30 * 1000;
/**
* If our login credentials are invalid then we will stop api requests until
* our configuration is changed
*/
private boolean invalidCredentials;
/**
* Use Craftman URL and APPID
*/
private boolean useCraftman = false;
/**
* Called by the SCR to activate the component with its configuration read
* from CAS
*
* @param bundleContext
* BundleContext of the Bundle that defines this component
* @param configuration
* Configuration properties for this component obtained from the
* ConfigAdmin service
*/
public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) {
this.bundleContext = bundleContext;
modified(configuration);
}
/**
* Called by the SCR when the configuration of a binding has been changed
* through the ConfigAdmin service.
*
* @param configuration
* Updated configuration properties
*/
public void modified(final Map<String, Object> configuration) {
String refreshIntervalString = Objects.toString(configuration.get("refresh"), null);
if (StringUtils.isNotBlank(refreshIntervalString)) {
refreshInterval = Long.parseLong(refreshIntervalString);
}
String quickrefreshIntervalString = Objects.toString(configuration.get("quickrefresh"), null);
if (StringUtils.isNotBlank(quickrefreshIntervalString)) {
rapidRefresh = Integer.parseInt(quickrefreshIntervalString);
}
// update the internal configuration accordingly
String usernameString = Objects.toString(configuration.get("username"), null);
String passwordString = Objects.toString(configuration.get("password"), null);
String appId = Objects.toString(configuration.get("appId"), null);
if (StringUtils.isBlank(appId)) {
appId = MyqData.DEFAULT_APP_ID;
}
int timeout = MyqData.DEFAUALT_TIMEOUT;
String timeoutString = Objects.toString(configuration.get("timeout"), null);
if (StringUtils.isNotBlank(timeoutString)) {
timeout = Integer.parseInt(timeoutString);
}
String craftmanString = Objects.toString(configuration.get("craftman"), null);
if (StringUtils.isNotBlank(craftmanString)) {
useCraftman = Boolean.parseBoolean(craftmanString);
}
// reinitialize connection object if username and password is changed
if (StringUtils.isNotBlank(usernameString) && StringUtils.isNotBlank(passwordString)) {
myqOnlineData = new MyqData(usernameString, passwordString, appId, timeout, useCraftman);
invalidCredentials = false;
schedulePoll(refreshInterval);
}
}
/**
* Called by the SCR to deactivate the component when either the
* configuration is removed or mandatory references are no longer satisfied
* or the component has simply been stopped.
*
* @param reason
* Reason code for the deactivation:<br>
* <ul>
* <li>0 – Unspecified
* <li>1 – The component was disabled
* <li>2 – A reference became unsatisfied
* <li>3 – A configuration was changed
* <li>4 – A configuration was deleted
* <li>5 – The component was disposed
* <li>6 – The bundle was stopped
* </ul>
*/
public void deactivate(final int reason) {
this.bundleContext = null;
// deallocate resources here that are no longer needed and
// should be reset when activating this binding again
if (pollFuture != null && !pollFuture.isCancelled()) {
pollFuture.cancel(true);
}
if (pollResetFuture != null && !pollResetFuture.isCancelled()) {
pollResetFuture.cancel(true);
}
}
/**
* Poll for device changes
*/
private void poll() {
if (invalidCredentials || this.myqOnlineData == null) {
logger.trace("Invalid Account Credentials");
return;
}
try {
// Get myQ Data
MyqDeviceData myqStatus = myqOnlineData.getMyqData();
for (MyqBindingProvider provider : providers) {
for (String mygItemName : provider.getInBindingItemNames()) {
MyqBindingConfig deviceConfig = getConfigForItemName(mygItemName);
if (deviceConfig != null) {
MyqDevice device = myqStatus.getDevice(deviceConfig.deviceIndex);
if (device != null) {
if (device instanceof GarageDoorDevice) {
GarageDoorDevice garageopener = (GarageDoorDevice) device;
State newState = UnDefType.UNDEF;
if (!deviceConfig.attribute.isEmpty()
&& garageopener.hasAttribute(deviceConfig.attribute)) {
newState = TypeParser.parseState(deviceConfig.acceptedDataTypes,
garageopener.getAttribute(deviceConfig.attribute));
if (newState == null) {
newState = UnDefType.UNDEF;
}
} else {
for (Class<? extends State> type : deviceConfig.acceptedDataTypes) {
if (OpenClosedType.class == type) {
if (garageopener.getStatus().isClosed()) {
newState = OpenClosedType.CLOSED;
break;
} else {
newState = OpenClosedType.OPEN;
break;
}
} else if (UpDownType.class == type) {
if (garageopener.getStatus().isClosed()) {
newState = UpDownType.DOWN;
break;
} else if (garageopener.getStatus().isOpen()) {
newState = UpDownType.UP;
break;
}
} else if (OnOffType.class == type) {
if (garageopener.getStatus().isClosed()) {
newState = OnOffType.OFF;
break;
} else {
newState = OnOffType.ON;
break;
}
} else if (PercentType.class == type) {
if (garageopener.getStatus().isClosed()) {
newState = PercentType.HUNDRED;
break;
} else if (garageopener.getStatus().isOpen()) {
newState = PercentType.ZERO;
break;
} else if (garageopener.getStatus().inMotion()) {
newState = new PercentType(50);
break;
}
} else if (StringType.class == type) {
newState = new StringType(garageopener.getStatus().getLabel());
break;
}
}
}
eventPublisher.postUpdate(mygItemName, newState);
// make sure we are polling frequently
if (garageopener.getStatus().inMotion()) {
beginRapidPoll(false);
}
} else if (device instanceof LampDevice) {
LampDevice lampDevice = (LampDevice) device;
State newState = UnDefType.UNDEF;
if (!deviceConfig.attribute.isEmpty()
&& lampDevice.hasAttribute(deviceConfig.attribute)) {
newState = TypeParser.parseState(deviceConfig.acceptedDataTypes,
lampDevice.getAttribute(deviceConfig.attribute));
if (newState == null) {
newState = UnDefType.UNDEF;
}
} else {
for (Class<? extends State> type : deviceConfig.acceptedDataTypes) {
if (OnOffType.class == type) {
newState = lampDevice.getState();
break;
}
}
}
eventPublisher.postUpdate(mygItemName, newState);
}
}
}
}
}
} catch (InvalidLoginException e) {
logger.error("Could not log in, please check your credentials.", e);
invalidCredentials = true;
} catch (IOException e) {
logger.error("Could not connect to MyQ service", e);
}
}
/**
* @{inheritDoc}
*/
@Override
public void internalReceiveCommand(String itemName, Command command) {
super.internalReceiveCommand(itemName, command);
logger.trace("MyQ binding received command '{}' for item '{}'", command, itemName);
if (myqOnlineData != null) {
computeCommandForItem(command, itemName);
} else {
logger.warn("Command '{}' for item '{}' not sent", command, itemName);
}
}
/**
* Checks whether the command is value and if the deviceID exists then get
* status of Garage Door Opener and send command to change it's state
* opposite of its current state
*
* @param command
* The command from the openHAB bus.
* @param itemName
* The name of the targeted item.
*/
private void computeCommandForItem(Command command, String itemName) {
MyqBindingConfig deviceConfig = getConfigForItemName(itemName);
if (invalidCredentials || deviceConfig == null) {
return;
}
try {
MyqDeviceData myqStatus = myqOnlineData.getMyqData();
MyqDevice device = myqStatus.getDevice(deviceConfig.deviceIndex);
if (device != null) {
if (device instanceof GarageDoorDevice) {
GarageDoorDevice garageopener = (GarageDoorDevice) device;
if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)) {
myqOnlineData.executeMyQCommand(garageopener.getDeviceId(), "desireddoorstate", 1);
beginRapidPoll(true);
} else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN)) {
myqOnlineData.executeMyQCommand(garageopener.getDeviceId(), "desireddoorstate", 0);
beginRapidPoll(true);
} else {
logger.warn("Unknown command {}", command);
}
} else if (device instanceof LampDevice) {
LampDevice lampModule = (LampDevice) device;
if (command.equals(OnOffType.ON)) {
myqOnlineData.executeMyQCommand(lampModule.getDeviceId(), "desiredlightstate", 1);
doFuturePoll(rapidRefresh);
} else if (command.equals(OnOffType.OFF)) {
myqOnlineData.executeMyQCommand(lampModule.getDeviceId(), "desiredlightstate", 0);
doFuturePoll(rapidRefresh);
} else {
logger.warn("Unknown command {}", command);
}
}
} else {
logger.warn("no MyQ device found with index: {}", deviceConfig.deviceIndex);
}
} catch (InvalidLoginException e) {
logger.error("Could not log in, please check your credentials.", e);
invalidCredentials = true;
} catch (IOException e) {
logger.error("Could not connect to MyQ service", e);
}
}
/**
* get item config based on item name(copied from HUE binding)
*/
private MyqBindingConfig getConfigForItemName(String itemName) {
for (MyqBindingProvider provider : providers) {
if (provider.getItemConfig(itemName) != null) {
return provider.getItemConfig(itemName);
}
}
return null;
}
/**
* Schedule our polling task
*
* @param millis
*/
private void schedulePoll(long millis) {
if (pollFuture != null && !pollFuture.isCancelled()) {
pollFuture.cancel(false);
}
logger.trace("rapidRefreshFuture scheduling for {} millis", millis);
// start polling at the RAPID_REFRESH_SECS interval
pollFuture = pollService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
poll();
}
}, 0, millis, TimeUnit.MILLISECONDS);
}
/**
* Schedule the task to reset out poll rate in a future time
*/
private void scheduleFuturePollReset() {
// stop rapid polling after MAX_RAPID_REFRESH_SECS
pollResetFuture = pollService.schedule(new Runnable() {
@Override
public void run() {
logger.trace("rapidRefreshFutureEnd stopping");
schedulePoll(refreshInterval);
}
}, MAX_RAPID_REFRESH, TimeUnit.MILLISECONDS);
}
/**
* Start rapid polling
*
* @param restart
* if already running, otherwise ignore.
*/
private void beginRapidPoll(boolean restart) {
if (restart && pollResetFuture != null) {
pollResetFuture.cancel(true);
pollResetFuture = null;
}
if (pollResetFuture == null || pollResetFuture.isCancelled()) {
schedulePoll(rapidRefresh);
scheduleFuturePollReset();
}
}
/**
* schedule a Poll in the near future
*/
private void doFuturePoll(long millis) {
pollResetFuture = pollService.schedule(new Runnable() {
@Override
public void run() {
logger.trace("do schedule poll");
poll();
}
}, millis, TimeUnit.MILLISECONDS);
}
}