/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* 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.frontiersiliconradio.internal;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.frontiersiliconradio.FrontierSiliconRadioBindingProvider;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
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.UnDefType;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Binding for radios based on the Fontier Silicon Chipset. It is a simple refresh-based binding that updates item
* providers. Moreover, a {@link #cachePeriod} can be used to cache the item states so that the event bus is not flooded
* with events that do not change any state.
*
* @author Rainer Ostendorf
* @author paphko
* @since 1.7.0
*/
public class FrontierSiliconRadioBinding extends AbstractActiveBinding<FrontierSiliconRadioBindingProvider>
implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(FrontierSiliconRadioBinding.class);
/** RegEx to validate a config <code>'^(.*?)\\.?(host|port|pin|refreshInterval|cachePeriod)$'</code> */
private static final Pattern EXTRACT_CONFIG_PATTERN = Pattern
.compile("^(.*?)\\.?(host|port|pin|refreshInterval|cachePeriod)$");
/** default tcp port (http). Make this configurable cause maybe people do NAT/portforwarding. */
private static final int DEFAULT_PORT = 80;
/** the default radio pin. HAMA radio was delivered with '1234' **/
private static final String DEFAULT_PIN = "1234";
/**
* the refresh interval which is used to poll values from the FrontierSiliconRadio server (optional, defaults to
* 60000ms = 1min)
*/
private long refreshInterval = 60000;
/** If caching is enabled, keep cache for this amount of minutes. */
private long cachePeriod = 0;
/** Remember last time the cache was purged. */
private long lastCachePurge = 0;
/** Map table to store all available radios configured by the user */
private final Map<String, FrontierSiliconRadioBindingConfig> deviceConfigCache = new HashMap<String, FrontierSiliconRadioBindingConfig>();
/** Cache all values so that we do not spam the event bus in case nothing changed. */
private final Map<String, Map<String, Object>> deviceToStateMap = new HashMap<String, Map<String, Object>>();
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
@Override
protected String getName() {
return "FrontierSiliconRadio Service";
}
@Override
protected void execute() {
long now = System.currentTimeMillis();
// clear cache after <cachePeriod> minutes
if (cachePeriod > 0 && lastCachePurge + (cachePeriod * 60000) < now) {
logger.debug("Clearing cache because it was older than " + cachePeriod + " minutes.");
deviceToStateMap.clear();
lastCachePurge = now;
}
for (FrontierSiliconRadioBindingProvider provider : providers) {
updateProvider(provider);
}
}
/**
* Update all items of the given item provider.
*/
private void updateProvider(FrontierSiliconRadioBindingProvider provider) {
for (String itemName : provider.getItemNames()) {
// find the matching device config for this item
final String deviceId = provider.getDeviceID(itemName);
if (deviceId == null) {
logger.error("could not find deviceId of item: " + itemName);
continue;
} else {
final FrontierSiliconRadioBindingConfig deviceConf = deviceConfigCache.get(deviceId);
if (deviceConf != null) {
// get the assigned radio and its state (on or off)
final FrontierSiliconRadio radio = deviceConf.getRadio();
final boolean powerState = radio.getPower();
// get the assigned property of this item
final String property = provider.getProperty(itemName);
// if radio is OFF, set all values (except of power item) to uninitialized
if (!powerState && !"POWER".equals(property)) {
if (stateChanged(deviceId, property, null)) {
eventPublisher.postUpdate(itemName, UnDefType.UNDEF);
}
continue; // continue with next item
}
// depending on the selected property, poll the property from the radio and update the item
switch (property) {
case "POWER":
if (stateChanged(deviceId, property, powerState)) {
logger.debug("powerState changed to " + powerState);
eventPublisher.postUpdate(itemName, powerState ? OnOffType.ON : OnOffType.OFF);
}
break;
case "MODE":
final int mode = radio.getMode();
if (stateChanged(deviceId, property, mode)) {
logger.debug("powerState changed to " + mode);
eventPublisher.postUpdate(itemName, new DecimalType(mode));
}
break;
case "VOLUME":
final int volume = radio.getVolume();
if (stateChanged(deviceId, property, volume)) {
final int percent = radio.convertVolumeToPercent(volume);
logger.debug("volume changed to " + volume + " (" + percent + "%)");
// based on the item type, either set absolue value or percent value
if (provider.getItemType(itemName) == DimmerItem.class) {
eventPublisher.postUpdate(itemName, new PercentType(percent));
} else {
eventPublisher.postUpdate(itemName, new DecimalType(volume));
}
}
break;
case "PLAYINFONAME":
final String playInfoName = radio.getPlayInfoName();
if (stateChanged(deviceId, property, playInfoName)) {
logger.debug("play info name changed to " + playInfoName);
eventPublisher.postUpdate(itemName, new StringType(playInfoName));
}
break;
case "PLAYINFOTEXT":
final String playInfoText = radio.getPlayInfoText();
if (stateChanged(deviceId, property, playInfoText)) {
logger.debug("play info text changed to " + playInfoText);
eventPublisher.postUpdate(itemName, new StringType(playInfoText));
}
break;
case "PRESET":
// preset is write-only, ignore
break;
case "MUTE":
final boolean muteState = radio.getMuted();
if (stateChanged(deviceId, property, muteState)) {
logger.debug("mute state changed to " + muteState);
eventPublisher.postUpdate(itemName, muteState ? OnOffType.ON : OnOffType.OFF);
}
break;
default:
logger.error("unknown property: '" + property + "'");
}
} else {
logger.error("deviceConf is null, no config found for deviceId: '" + deviceId
+ "'. Check binding config.");
}
}
}
}
/**
* If {@link #cachePeriod} is set, this call checks whether the value changed. In the end, <code>newValue</code> is
* cached so that it can be compared in subsequent calls.
*
* @param deviceId
* The radio id.
* @param property
* The property that is checked for update.
* @param newValue
* The new value of the property.
* @return <code>true</code> if the value changed compared to the last call or {@link #cachePeriod} is not set;
* <code>false</code> if the value did not changed compared to the last call and {@link #cachePeriod} is
* set.
*/
private boolean stateChanged(String deviceId, String property, Object newValue) {
if (cachePeriod <= 0) {
return true; // no caching
}
final Map<String, Object> map = deviceToStateMap.get(deviceId);
if (map == null) {
final Map<String, Object> newMap = new HashMap<String, Object>();
newMap.put(property, newValue);
deviceToStateMap.put(deviceId, newMap);
return true;
}
final Object oldValue = map.get(property);
if (oldValue == null ? newValue != null : !oldValue.equals(newValue)) {
map.put(property, newValue);
return true;
}
return false;
}
@Override
protected void internalReceiveCommand(String itemName, Command command) {
for (final FrontierSiliconRadioBindingProvider provider : providers) {
for (final String providerItemName : provider.getItemNames()) {
if (providerItemName.equals(itemName)) {
handleReceiveCommand(provider, itemName, command);
}
}
}
}
/**
* Handle the command received for the given item provider and item. If the command switches the power state of the
* radio, all other items are updated afterwards independent of the next refresh call.
*
* @param provider
* @param itemName
* @param command
*/
private void handleReceiveCommand(final FrontierSiliconRadioBindingProvider provider, final String itemName,
final Command command) {
// find the matching device config for this item
final String deviceId = provider.getDeviceID(itemName);
if (deviceId == null) {
logger.error("could not find deviceId of item: " + itemName);
return;
}
// try to get the config of the radio from our config cache
final FrontierSiliconRadioBindingConfig deviceConf = deviceConfigCache.get(deviceId);
if (deviceConf != null) {
// get the assigned radio
final FrontierSiliconRadio radio = deviceConf.getRadio();
// get the assigned property for this item
final String property = provider.getProperty(itemName);
// according the the assigned property, send the command to the assigned radio.
switch (property) {
case "POWER":
if (command.equals(OnOffType.ON) || command.equals(OpenClosedType.CLOSED)) {
radio.setPower(true);
} else {
radio.setPower(false);
}
// now all items should be updated! (wait some seconds so that text items are up-to-date)
new Thread(new Runnable() {
@Override
public void run() {
try {
// let's hope 4 seconds are enough...
Thread.sleep(4000);
} catch (InterruptedException e) {
}
updateProvider(provider);
}
}).start();
break;
case "VOLUME":
if (command instanceof IncreaseDecreaseType) {
if (command.equals(IncreaseDecreaseType.INCREASE)) {
radio.increaseVolume();
} else {
radio.decreaseVolume();
}
} else if (command instanceof UpDownType) {
if (command.equals(UpDownType.UP)) {
radio.increaseVolume();
} else {
radio.decreaseVolume();
}
} else if (command instanceof PercentType) {
final Integer percentValue = ((DecimalType) command).intValue();
final Integer absoluteValue = radio.convertPercentToVolume(percentValue);
radio.setVolume(absoluteValue);
} else if (command instanceof DecimalType) {
final Integer intValue = ((DecimalType) command).intValue();
radio.setVolume(intValue);
}
break;
case "MODE":
final Integer mode = ((DecimalType) command).intValue();
radio.setMode(mode);
break;
case "PRESET":
final Integer preset = ((DecimalType) command).intValue();
radio.setPreset(preset);
break;
case "MUTE":
if (command.equals(OnOffType.ON) || command.equals(OpenClosedType.CLOSED)) {
radio.setMuted(true);
} else {
radio.setMuted(false);
}
break;
default:
logger.error(
"command on unknown property: '" + property + "'. Maybe trying to set read-only property?");
break;
}
}
}
protected void addBindingProvider(FrontierSiliconRadioBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(FrontierSiliconRadioBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
@Override
public void updated(Dictionary<String, ?> config) throws ConfigurationException {
deviceConfigCache.clear();
if (config != null) {
logger.debug("Configuration updated with " + config.size() + " keys");
final Enumeration<String> keys = config.keys();
while (keys.hasMoreElements()) {
final String key = keys.nextElement();
final Matcher matcher = EXTRACT_CONFIG_PATTERN.matcher(key);
if (!matcher.matches()) {
if (!"service.pid".equals(key)) {
logger.debug("given config key '" + key
+ "' does not follow the expected pattern '<id>.<host|port|pin|refreshInterval|cachePeriod>'");
}
continue;
} else {
logger.debug("matching config item found: " + key + " = " + config.get(key));
}
// regex: "^(.*?)\\.?(host|port|pin|refreshInterval|cachePeriod)$"
final String deviceId = matcher.group(1);
final String configKey = matcher.group(2);
final String value = (String) config.get(key);
if (deviceId == null || deviceId.trim().isEmpty()) {
// general config
if ("cachePeriod".equalsIgnoreCase(configKey)) {
logger.debug("Cache period is " + value);
cachePeriod = Integer.parseInt(value.trim());
} else if ("refreshInterval".equalsIgnoreCase(configKey)) {
logger.debug("Refresh interval is " + value);
refreshInterval = Long.parseLong(value.trim());
} else {
logger.error("the given config key '" + configKey + "' is unknown");
}
} else {
// device-specific config
FrontierSiliconRadioBindingConfig deviceConfig = deviceConfigCache.get(deviceId);
if (deviceConfig == null) {
deviceConfig = new FrontierSiliconRadioBindingConfig(deviceId);
deviceConfigCache.put(deviceId, deviceConfig);
}
if ("host".equalsIgnoreCase(configKey)) {
logger.debug("Host name for " + deviceId + " is " + value);
deviceConfig.host = value;
} else if ("port".equalsIgnoreCase(configKey)) {
logger.debug("Port number for " + deviceId + " is " + value);
deviceConfig.port = Integer.valueOf(value.trim());
} else if ("pin".equalsIgnoreCase(configKey)) {
logger.debug("PIN for " + deviceId + " is " + value);
deviceConfig.pin = value;
} else {
logger.error("the given config key '" + configKey + "' is unknown");
}
}
}
// open connection to radio
for (String device : deviceConfigCache.keySet()) {
final FrontierSiliconRadio radio = deviceConfigCache.get(device).getRadio();
if (radio != null) {
radio.login();
}
}
}
setProperlyConfigured(!deviceConfigCache.isEmpty());
}
/**
* Holds the binding configuration, consisting of: - deviceID (e.g. "sleepingroom") - host (e.g. "192.167.2.23" ) -
* portnumber (defaults to 80) - Access PIN (e.g. 1234)
*/
private class FrontierSiliconRadioBindingConfig {
private final String deviceId; // the end point identifier, e.g. "RadioKitchen"
private String host; // host name or ip
private int port = DEFAULT_PORT; // TCP port number
private String pin = DEFAULT_PIN;
private FrontierSiliconRadio radio = null;
public FrontierSiliconRadioBindingConfig(String deviceId) {
this.deviceId = deviceId;
}
@Override
public String toString() {
return "Device [id=" + deviceId + ", host=" + host + ", port=" + port + ", pin: " + pin + "]";
}
public FrontierSiliconRadio getRadio() {
if (radio == null) {
logger.debug("creating new connection to " + host + ":" + port);
radio = new FrontierSiliconRadio(host, port, pin);
}
return radio;
}
}
}