added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.meteostick-${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-meteostick" description="Meteostick Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.meteostick/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 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.meteostick.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link meteostickBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Chris Jackson - Initial contribution
*/
@NonNullByDefault
public class MeteostickBindingConstants {
public static final String BINDING_ID = "meteostick";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "meteostick_bridge");
public static final ThingTypeUID THING_TYPE_DAVIS = new ThingTypeUID(BINDING_ID, "meteostick_davis_iss");
// List of all Channel ids
public static final String CHANNEL_INDOOR_TEMPERATURE = "indoor-temperature";
public static final String CHANNEL_OUTDOOR_TEMPERATURE = "outdoor-temperature";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_PRESSURE = "pressure";
public static final String CHANNEL_RAIN_RAW = "rain-raw";
public static final String CHANNEL_RAIN_CURRENTHOUR = "rain-currenthour";
public static final String CHANNEL_RAIN_LASTHOUR = "rain-lasthour";
public static final String CHANNEL_WIND_SPEED = "wind-speed";
public static final String CHANNEL_WIND_DIRECTION = "wind-direction";
public static final String CHANNEL_WIND_SPEED_LAST2MIN_AVERAGE = "wind-speed-last2min-average";
public static final String CHANNEL_WIND_SPEED_LAST2MIN_MAXIMUM = "wind-speed-last2min-maximum";
public static final String CHANNEL_WIND_DIRECTION_LAST2MIN_AVERAGE = "wind-direction-last2min-average";
public static final String CHANNEL_SOLAR_POWER = "solar-power";
public static final String CHANNEL_SIGNAL_STRENGTH = "signal-strength";
public static final String CHANNEL_LOW_BATTERY = "low-battery";
// List of parameters
public static final String PARAMETER_CHANNEL = "channel";
public static final String PARAMETER_SPOON = "spoon";
public static final String PARAMETER_SPOON_DEFAULT = "0.254";
// Miscellaneous constants
public static final long HOUR_IN_SEC = 60 * 60;
public static final long HOUR_IN_MSEC = HOUR_IN_SEC * 1000;
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 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.meteostick.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.meteostick.internal.handler.MeteostickBridgeHandler;
import org.openhab.binding.meteostick.internal.handler.MeteostickSensorHandler;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MeteostickHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Chris Jackson - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.meteostick")
public class MeteostickHandlerFactory extends BaseThingHandlerFactory {
private Logger logger = LoggerFactory.getLogger(MeteostickHandlerFactory.class);
private final SerialPortManager serialPortManager;
@Activate
public MeteostickHandlerFactory(final @Reference SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return MeteostickBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)
| MeteostickSensorHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
logger.debug("MeteoStick thing factory: createHandler {} of type {}", thing.getThingTypeUID(), thing.getUID());
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (MeteostickBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new MeteostickBridgeHandler((Bridge) thing, serialPortManager);
}
if (MeteostickSensorHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new MeteostickSensorHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,344 @@
/**
* Copyright (c) 2010-2020 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.meteostick.internal.handler;
import static org.openhab.binding.meteostick.internal.MeteostickBindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.HECTO;
import static org.openhab.core.library.unit.SIUnits.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.TooManyListenersException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortEvent;
import org.openhab.core.io.transport.serial.SerialPortEventListener;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MeteostickBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Chris Jackson - Initial contribution
*/
public class MeteostickBridgeHandler extends BaseBridgeHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
private final Logger logger = LoggerFactory.getLogger(MeteostickBridgeHandler.class);
private static final int RECEIVE_TIMEOUT = 3000;
private SerialPort serialPort;
private final SerialPortManager serialPortManager;
private ReceiveThread receiveThread;
private ScheduledFuture<?> offlineTimerJob;
private String meteostickMode = "m1";
private final String meteostickFormat = "o1";
private Date lastData;
private ConcurrentMap<Integer, MeteostickEventListener> eventListeners = new ConcurrentHashMap<>();
public MeteostickBridgeHandler(Bridge thing, SerialPortManager serialPortManager) {
super(thing);
this.serialPortManager = serialPortManager;
}
@Override
public void initialize() {
logger.debug("Initializing MeteoStick Bridge handler.");
updateStatus(ThingStatus.UNKNOWN);
Configuration config = getThing().getConfiguration();
final String port = (String) config.get("port");
final BigDecimal mode = (BigDecimal) config.get("mode");
if (mode != null) {
meteostickMode = "m" + mode.toString();
}
Runnable pollingRunnable = () -> {
if (connectPort(port)) {
offlineTimerJob.cancel(true);
}
};
// Scheduling a job on each hour to update the last hour rainfall
offlineTimerJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, 60, TimeUnit.SECONDS);
}
@Override
public void dispose() {
disconnect();
if (offlineTimerJob != null) {
offlineTimerJob.cancel(true);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
private void resetMeteoStick() {
sendToMeteostick("r");
}
protected void subscribeEvents(int channel, MeteostickEventListener handler) {
logger.debug("MeteoStick bridge: subscribeEvents to channel {} with {}", channel, handler);
if (eventListeners.containsKey(channel)) {
logger.debug("MeteoStick bridge: subscribeEvents to channel {} already registered", channel);
return;
}
eventListeners.put(channel, handler);
resetMeteoStick();
}
protected void unsubscribeEvents(int channel, MeteostickEventListener handler) {
logger.debug("MeteoStick bridge: unsubscribeEvents to channel {} with {}", channel, handler);
eventListeners.remove(channel, handler);
resetMeteoStick();
}
/**
* Connects to the comm port and starts send and receive threads.
*
* @param serialPortName the port name to open
* @throws SerialInterfaceException when a connection error occurs.
*/
private boolean connectPort(final String serialPortName) {
logger.debug("MeteoStick Connecting to serial port {}", serialPortName);
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
if (portIdentifier == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Serial Error: Port " + serialPortName + " does not exist");
return false;
}
boolean success = false;
try {
serialPort = portIdentifier.open("org.openhab.binding.meteostick", 2000);
serialPort.setSerialPortParams(115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
serialPort.enableReceiveThreshold(1);
serialPort.enableReceiveTimeout(RECEIVE_TIMEOUT);
receiveThread = new ReceiveThread();
receiveThread.start();
// RXTX serial port library causes high CPU load
// Start event listener, which will just sleep and slow down event loop
serialPort.addEventListener(this.receiveThread);
serialPort.notifyOnDataAvailable(true);
logger.debug("Serial port is initialized");
success = true;
} catch (PortInUseException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Serial Error: Port " + serialPortName + " in use");
} catch (UnsupportedCommOperationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Serial Error: Unsupported comm operation on port " + serialPortName);
} catch (TooManyListenersException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"Serial Error: Too many listeners on port " + serialPortName);
}
return success;
}
/**
* Disconnects from the serial interface and stops send and receive threads.
*/
private void disconnect() {
if (receiveThread != null) {
receiveThread.interrupt();
try {
receiveThread.join();
} catch (InterruptedException e) {
}
receiveThread = null;
}
if (this.serialPort != null) {
this.serialPort.close();
this.serialPort = null;
}
logger.debug("Disconnected from serial port");
}
private void sendToMeteostick(String string) {
try {
synchronized (serialPort.getOutputStream()) {
serialPort.getOutputStream().write(string.getBytes());
serialPort.getOutputStream().write(13);
serialPort.getOutputStream().flush();
}
} catch (IOException e) {
logger.error("Got I/O exception {} during sending. exiting thread.", e.getLocalizedMessage());
}
}
private class ReceiveThread extends Thread implements SerialPortEventListener {
private final Logger logger = LoggerFactory.getLogger(ReceiveThread.class);
@Override
public void serialEvent(SerialPortEvent arg0) {
try {
logger.trace("RXTX library CPU load workaround, sleep forever");
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
}
}
/**
* Run method. Runs the actual receiving process.
*/
@Override
public void run() {
logger.debug("Starting MeteoStick Receive Thread");
byte[] rxPacket = new byte[100];
int rxCnt = 0;
int rxByte;
while (!interrupted()) {
try {
rxByte = serialPort.getInputStream().read();
if (rxByte == -1) {
continue;
}
lastData = new Date();
startTimeoutCheck();
// Check for end of line
if (rxByte == 13 && rxCnt > 0) {
String inputString = new String(rxPacket, 0, rxCnt);
logger.debug("MeteoStick received: {}", inputString);
String p[] = inputString.split("\\s+");
switch (p[0]) {
case "B": // Barometer
BigDecimal temperature = new BigDecimal(p[1]);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_INDOOR_TEMPERATURE),
new QuantityType<>(temperature.setScale(1), CELSIUS));
BigDecimal pressure = new BigDecimal(p[2]);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PRESSURE),
new QuantityType<>(pressure.setScale(1, RoundingMode.HALF_UP), HECTO(PASCAL)));
break;
case "#":
break;
case "?":
// Create the channel command
int channels = 0;
for (int channel : eventListeners.keySet()) {
channels += Math.pow(2, channel - 1);
}
// Device has been reset - reconfigure
sendToMeteostick(meteostickFormat);
sendToMeteostick(meteostickMode);
sendToMeteostick("t" + channels);
break;
default:
if (p.length < 3) {
logger.debug("MeteoStick bridge: short data ({})", p.length);
break;
}
try {
MeteostickEventListener listener = eventListeners.get(Integer.parseInt(p[1]));
if (listener != null) {
listener.onDataReceived(p);
} else {
logger.debug("MeteoStick bridge: data from channel {} with no handler",
Integer.parseInt(p[1]));
}
} catch (NumberFormatException e) {
}
break;
}
updateStatus(ThingStatus.ONLINE);
rxCnt = 0;
} else if (rxByte != 10) {
// Ignore line feed
rxPacket[rxCnt] = (byte) rxByte;
if (rxCnt < rxPacket.length) {
rxCnt++;
}
}
} catch (Exception e) {
rxCnt = 0;
logger.error("Exception during MeteoStick receive thread", e);
}
}
logger.debug("Stopping MeteoStick Receive Thread");
serialPort.removeEventListener();
}
}
private synchronized void startTimeoutCheck() {
Runnable pollingRunnable = () -> {
String detail;
if (lastData == null) {
detail = "No data received";
} else {
detail = "No data received since " + lastData.toString();
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, detail);
};
if (offlineTimerJob != null) {
offlineTimerJob.cancel(true);
}
// Scheduling a job on each hour to update the last hour rainfall
offlineTimerJob = scheduler.schedule(pollingRunnable, 90, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 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.meteostick.internal.handler;
/**
* This interface provides notifications between the bridge and the sensors.
*
* @author Chris Jackson - Initial contribution
*
*/
public interface MeteostickEventListener {
/**
* Called each time a new line of data is received
*
* @param data a line of data from the meteoStick
*/
public void onDataReceived(String data[]);
}

View File

@@ -0,0 +1,395 @@
/**
* Copyright (c) 2010-2020 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.meteostick.internal.handler;
import static org.openhab.binding.meteostick.internal.MeteostickBindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.MILLI;
import static org.openhab.core.library.unit.SIUnits.*;
import static org.openhab.core.library.unit.SmartHomeUnits.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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.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.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MeteostickSensorHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Chris Jackson - Initial contribution
* @author John Cocula - Added variable spoon size, UoM, wind stats, bug fixes
*/
public class MeteostickSensorHandler extends BaseThingHandler implements MeteostickEventListener {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_DAVIS);
private final Logger logger = LoggerFactory.getLogger(MeteostickSensorHandler.class);
private int channel = 0;
private BigDecimal spoon = new BigDecimal(PARAMETER_SPOON_DEFAULT);
private MeteostickBridgeHandler bridgeHandler;
private RainHistory rainHistory = new RainHistory(HOUR_IN_MSEC);
private WindHistory windHistory = new WindHistory(2 * 60 * 1000); // 2 minutes
private ScheduledFuture<?> rainHourlyJob;
private ScheduledFuture<?> wind2MinJob;
private ScheduledFuture<?> offlineTimerJob;
private Date lastData;
public MeteostickSensorHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
logger.debug("Initializing MeteoStick handler.");
channel = ((BigDecimal) getConfig().get(PARAMETER_CHANNEL)).intValue();
spoon = (BigDecimal) getConfig().get(PARAMETER_SPOON);
if (spoon == null) {
spoon = new BigDecimal(PARAMETER_SPOON_DEFAULT);
}
logger.debug("Initializing MeteoStick handler - Channel {}, Spoon size {} mm.", channel, spoon);
Runnable rainRunnable = () -> {
BigDecimal rainfall = rainHistory.getTotal(spoon);
rainfall.setScale(1, RoundingMode.DOWN);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_RAIN_LASTHOUR),
new QuantityType<>(rainfall, MILLI(METRE)));
};
// Scheduling a job on each hour to update the last hour rainfall
long start = HOUR_IN_SEC - ((System.currentTimeMillis() % HOUR_IN_MSEC) / 1000);
rainHourlyJob = scheduler.scheduleWithFixedDelay(rainRunnable, start, HOUR_IN_SEC, TimeUnit.SECONDS);
Runnable windRunnable = () -> {
WindStats stats = windHistory.getStats();
updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_SPEED_LAST2MIN_AVERAGE),
new QuantityType<>(stats.averageSpeed, METRE_PER_SECOND));
updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_SPEED_LAST2MIN_MAXIMUM),
new QuantityType<>(stats.maxSpeed, METRE_PER_SECOND));
updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_DIRECTION_LAST2MIN_AVERAGE),
new QuantityType<>(stats.averageDirection, DEGREE_ANGLE));
};
// Scheduling a job to run every two minutes to update wind statistics
wind2MinJob = scheduler.scheduleWithFixedDelay(windRunnable, 2, 2, TimeUnit.MINUTES);
updateStatus(ThingStatus.UNKNOWN);
}
@Override
public void dispose() {
if (rainHourlyJob != null) {
rainHourlyJob.cancel(true);
}
if (wind2MinJob != null) {
wind2MinJob.cancel(true);
}
if (offlineTimerJob != null) {
offlineTimerJob.cancel(true);
}
if (bridgeHandler != null) {
bridgeHandler.unsubscribeEvents(channel, this);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("MeteoStick handler {}: bridgeStatusChanged to {}", channel, bridgeStatusInfo);
if (bridgeStatusInfo.getStatus() != ThingStatus.ONLINE) {
logger.debug("MeteoStick handler {}: bridgeStatusChanged but bridge offline", channel);
updateStatus(ThingStatus.OFFLINE);
return;
}
bridgeHandler = (MeteostickBridgeHandler) getBridge().getHandler();
if (channel != 0) {
if (bridgeHandler != null) {
bridgeHandler.subscribeEvents(channel, this);
}
}
// Put the thing online and start our "no data" timer
updateStatus(ThingStatus.ONLINE);
startTimeoutCheck();
}
private void processSignalStrength(String dbmString) {
double dbm = Double.parseDouble(dbmString);
int strength;
if (dbm > -60) {
strength = 4;
} else if (dbm > -70) {
strength = 3;
} else if (dbm > -80) {
strength = 2;
} else if (dbm > -90) {
strength = 1;
} else {
strength = 0;
}
updateState(new ChannelUID(getThing().getUID(), CHANNEL_SIGNAL_STRENGTH), new DecimalType(strength));
}
private void processBattery(boolean batteryLow) {
OnOffType state = batteryLow ? OnOffType.ON : OnOffType.OFF;
updateState(new ChannelUID(getThing().getUID(), CHANNEL_LOW_BATTERY), state);
}
@Override
public void onDataReceived(String[] data) {
logger.debug("MeteoStick received channel {}: {}", channel, data);
updateStatus(ThingStatus.ONLINE);
lastData = new Date();
startTimeoutCheck();
switch (data[0]) {
case "R": // Rain
int rain = Integer.parseInt(data[2]);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_RAIN_RAW), new DecimalType(rain));
processSignalStrength(data[3]);
processBattery(data.length == 5);
rainHistory.put(rain);
BigDecimal rainfall = rainHistory.getTotal(spoon);
rainfall.setScale(1, RoundingMode.DOWN);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_RAIN_CURRENTHOUR),
new QuantityType<>(rainfall, MILLI(METRE)));
break;
case "W": // Wind
BigDecimal windSpeed = new BigDecimal(data[2]);
int windDirection = Integer.parseInt(data[3]);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_SPEED),
new QuantityType<>(windSpeed, METRE_PER_SECOND));
updateState(new ChannelUID(getThing().getUID(), CHANNEL_WIND_DIRECTION),
new QuantityType<>(windDirection, DEGREE_ANGLE));
windHistory.put(windSpeed, windDirection);
processSignalStrength(data[4]);
processBattery(data.length == 6);
break;
case "T": // Temperature
BigDecimal temperature = new BigDecimal(data[2]);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_OUTDOOR_TEMPERATURE),
new QuantityType<>(temperature.setScale(1), CELSIUS));
BigDecimal humidity = new BigDecimal(data[3]);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_HUMIDITY),
new DecimalType(humidity.setScale(1)));
processSignalStrength(data[4]);
processBattery(data.length == 6);
break;
case "P": // Solar panel power
BigDecimal power = new BigDecimal(data[2]);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_SOLAR_POWER),
new DecimalType(power.setScale(1)));
processSignalStrength(data[3]);
processBattery(data.length == 5);
break;
}
}
class SlidingTimeWindow<T> {
private long period = 0;
protected final SortedMap<Long, T> storage = Collections.synchronizedSortedMap(new TreeMap<>());
/**
*
* @param period window period in milliseconds
*/
public SlidingTimeWindow(long period) {
this.period = period;
}
public void put(T value) {
storage.put(System.currentTimeMillis(), value);
}
public void removeOldEntries() {
long old = System.currentTimeMillis() - period;
synchronized (storage) {
for (Iterator<Long> iterator = storage.keySet().iterator(); iterator.hasNext();) {
long time = iterator.next();
if (time < old) {
iterator.remove();
}
}
}
}
}
class RainHistory extends SlidingTimeWindow<Integer> {
public RainHistory(long period) {
super(period);
}
public BigDecimal getTotal(BigDecimal spoon) {
removeOldEntries();
int least = -1;
int total = 0;
synchronized (storage) {
for (int value : storage.values()) {
/*
* Rain counters have been seen to wrap at 127 and also at 255.
* The Meteostick documentation only mentions 255 at the time of
* this writing. This potential difference is solved by having
* all rain counters wrap at 127 (0x7F) by removing the high bit.
*/
value &= 0x7F;
if (least == -1) {
least = value;
continue;
}
if (value < least) {
total = 128 - least + value;
} else {
total = value - least;
}
}
}
return BigDecimal.valueOf(total).multiply(spoon);
}
}
/**
* Store the wind direction as an east-west vector and a north-south vector
* so that an average direction can be calculated based on the wind speed
* at the time of the direction sample.
*/
class WindSample {
double speed;
double ewVector;
double nsVector;
public WindSample(BigDecimal speed, int directionDegrees) {
this.speed = speed.doubleValue();
double direction = Math.toRadians(directionDegrees);
this.ewVector = this.speed * Math.sin(direction);
this.nsVector = this.speed * Math.cos(direction);
}
}
class WindStats {
BigDecimal averageSpeed;
int averageDirection;
BigDecimal maxSpeed;
}
class WindHistory extends SlidingTimeWindow<WindSample> {
public WindHistory(long period) {
super(period);
}
public void put(BigDecimal speed, int directionDegrees) {
put(new WindSample(speed, directionDegrees));
}
public WindStats getStats() {
removeOldEntries();
double ewSum = 0;
double nsSum = 0;
double totalSpeed = 0;
double maxSpeed = 0;
int size = 0;
synchronized (storage) {
size = storage.size();
for (WindSample sample : storage.values()) {
ewSum += sample.ewVector;
nsSum += sample.nsVector;
totalSpeed += sample.speed;
if (sample.speed > maxSpeed) {
maxSpeed = sample.speed;
}
}
}
WindStats stats = new WindStats();
stats.averageDirection = (int) Math.toDegrees(Math.atan2(ewSum, nsSum));
if (stats.averageDirection < 0) {
stats.averageDirection += 360;
}
stats.averageSpeed = new BigDecimal(size > 0 ? totalSpeed / size : 0).setScale(3, RoundingMode.HALF_DOWN);
stats.maxSpeed = new BigDecimal(maxSpeed).setScale(3, RoundingMode.HALF_DOWN);
return stats;
}
}
private synchronized void startTimeoutCheck() {
Runnable pollingRunnable = () -> {
String detail;
if (lastData == null) {
detail = "No data received";
} else {
detail = "No data received since " + lastData.toString();
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, detail);
};
if (offlineTimerJob != null) {
offlineTimerJob.cancel(true);
}
// Scheduling a job on each hour to update the last hour rainfall
offlineTimerJob = scheduler.schedule(pollingRunnable, 90, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="meteostick" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>MeteoStick Binding</name>
<description>This is the binding for MeteoStick USB weather station receiver.</description>
<author>Chris Jackson</author>
</binding:binding>

View File

@@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="meteostick"
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">
<bridge-type id="meteostick_bridge">
<label>Meteostick USB Receiver</label>
<description>Meteostick USB Receiver</description>
<channels>
<channel id="pressure" typeId="pressure"/>
<channel id="indoor-temperature" typeId="indoor-temperature"/>
</channels>
<config-description>
<parameter name="port" type="text" required="true">
<label>Serial Port</label>
<context>serial-port</context>
<limitToOptions>false</limitToOptions>
<description>Serial port that the Meteostick is plugged into</description>
</parameter>
<parameter name="mode" type="integer" required="true">
<label>Frequency Band</label>
<description>Specifies the frequency mode including device, region and frequency that Meteostick will use</description>
<options>
<option value="0">Davis - North America (915MHz)</option>
<option value="1">Davis - Europe (868MHz)</option>
<option value="2">Davis - Australia (915Mhz)</option>
<option value="3">Fine Offset - North America (915MHz)</option>
<option value="4">Fine Offset - Europe (868MHz)</option>
<option value="5">Davis - New Zealand (931.5Mhz)</option>
</options>
<default>1</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="meteostick_davis_iss">
<supported-bridge-type-refs>
<bridge-type-ref id="meteostick_bridge"/>
</supported-bridge-type-refs>
<label>Davis Vantage Vue ISS</label>
<description>Davis Integrated Sensor Suite</description>
<channels>
<channel id="outdoor-temperature" typeId="outdoor-temperature"/>
<channel id="humidity" typeId="humidity"/>
<channel id="wind-direction" typeId="wind-direction"/>
<channel id="wind-direction-last2min-average" typeId="wind-direction-last2min-average"/>
<channel id="wind-speed" typeId="wind-speed"/>
<channel id="wind-speed-last2min-average" typeId="wind-speed-last2min-average"/>
<channel id="wind-speed-last2min-maximum" typeId="wind-speed-last2min-maximum"/>
<channel id="rain-raw" typeId="rain-raw"/>
<channel id="rain-currenthour" typeId="rain-currenthour"/>
<channel id="rain-lasthour" typeId="rain-lasthour"/>
<channel id="solar-power" typeId="solar-power"/>
<channel id="signal-strength" typeId="system.signal-strength"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels>
<config-description>
<parameter name="channel" type="integer" required="true">
<label>Channel</label>
<description>Specifies the channel the sensor is using</description>
<options>
<option value="1">Channel 1</option>
<option value="2">Channel 2</option>
<option value="3">Channel 3</option>
<option value="4">Channel 4</option>
<option value="5">Channel 5</option>
<option value="6">Channel 6</option>
<option value="7">Channel 7</option>
<option value="8">Channel 8</option>
</options>
</parameter>
<parameter name="spoon" type="decimal" required="false">
<label>Spoon</label>
<description>Specifies the amount of rain needed to tip spoon</description>
<default>0.254</default>
<unitLabel>mm</unitLabel>
</parameter>
</config-description>
</thing-type>
<channel-type id="indoor-temperature">
<item-type>Number:Temperature</item-type>
<label>Indoor Temperature</label>
<description>Current indoor temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%">
</state>
</channel-type>
<channel-type id="outdoor-temperature">
<item-type>Number:Temperature</item-type>
<label>Outdoor Temperature</label>
<description>Current outdoor temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%">
</state>
</channel-type>
<channel-type id="humidity">
<item-type>Number</item-type>
<label>Outdoor Humidity</label>
<description>Current humidity in percent</description>
<category>Humidity</category>
<state readOnly="true" pattern="%d %%">
</state>
</channel-type>
<channel-type id="pressure">
<item-type>Number:Pressure</item-type>
<label>Pressure</label>
<description>Current atmospheric pressure</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.1f %unit%">
</state>
</channel-type>
<channel-type id="wind-direction">
<item-type>Number:Angle</item-type>
<label>Wind Direction</label>
<description>Wind direction</description>
<category>Wind</category>
<state readOnly="true" pattern="%.0f %unit%">
</state>
</channel-type>
<channel-type id="wind-direction-last2min-average" advanced="true">
<item-type>Number:Angle</item-type>
<label>Wind Direction (Average)</label>
<description>Wind direction average over last two minutes</description>
<category>Wind</category>
<state readOnly="true" pattern="%.0f %unit%">
</state>
</channel-type>
<channel-type id="wind-speed">
<item-type>Number:Speed</item-type>
<label>Wind Speed</label>
<description>Wind speed</description>
<category>Wind</category>
<state readOnly="true" pattern="%.2f %unit%">
</state>
</channel-type>
<channel-type id="wind-speed-last2min-average" advanced="true">
<item-type>Number:Speed</item-type>
<label>Wind Speed (Average)</label>
<description>Wind speed average over last two minutes</description>
<category>Wind</category>
<state readOnly="true" pattern="%.2f %unit%">
</state>
</channel-type>
<channel-type id="wind-speed-last2min-maximum" advanced="true">
<item-type>Number:Speed</item-type>
<label>Wind Speed (Maximum)</label>
<description>Wind speed maximum over last two minutes</description>
<category>Wind</category>
<state readOnly="true" pattern="%.2f %unit%">
</state>
</channel-type>
<channel-type id="rain-raw" advanced="true">
<item-type>Number</item-type>
<label>Rainfall (Raw)</label>
<description>A counter between 0 and 255 in spoon-sized steps</description>
<category>Rain</category>
<state readOnly="true" pattern="%.1f">
</state>
</channel-type>
<channel-type id="rain-lasthour">
<item-type>Number:Length</item-type>
<label>Rainfall (previous Hour)</label>
<description>Rainfall in the previous hour</description>
<category>Rain</category>
<state readOnly="true" pattern="%.1f %unit%">
</state>
</channel-type>
<channel-type id="rain-currenthour">
<item-type>Number:Length</item-type>
<label>Rainfall (60 Minutes)</label>
<description>Rainfall in the last 60 minutes</description>
<category>Rain</category>
<state readOnly="true" pattern="%.1f %unit%">
</state>
</channel-type>
<channel-type id="solar-power">
<item-type>Number</item-type>
<label>Solar Power</label>
<description>Solar panel power percentage</description>
<category>Light</category>
<state readOnly="true" pattern="%.1f %%">
</state>
</channel-type>
</thing:thing-descriptions>