[ipobserver] Weather station binding, Initial contribution. (#10567)

* Bulk updated to UOM.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* ipObserver creation


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Bulk updated to UOM.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* channel fixup for UOM.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* improve UOM.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* updates


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Battery ch fixed.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix time channels.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* readme update and remove %unit% from rain channels.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* readme fixup.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* edit global files.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix merge conflicts.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* fix up build issues.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* remove reboot channel.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* readme fixup.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Rename channels to put kind first.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* update to build on latest main.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Add support for outBatt1


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Added auto discovery.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* add bundle to POM.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* newline added.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix bug in discovery.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Added tags

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* update to 3.2.0-SNAPSHOT


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.ipobserver/src/main/resources/OH-INF/thing/thing-types.xml

Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* Update bundles/org.openhab.binding.ipobserver/src/main/resources/OH-INF/thing/thing-types.xml

Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* Clean up channels

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Update binding description.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix jsoup suggestions.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.ipobserver/src/main/java/org/openhab/binding/ipobserver/internal/IpObserverDiscoveryService.java

Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* Update bundles/org.openhab.binding.ipobserver/src/main/resources/OH-INF/thing/thing-types.xml

Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* Removed nullable.

Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Improvements


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Fix compiler warnings


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Change to datetime


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* change to use system channels.


Signed-off-by: Matthew Skinner <matt@pcmus.com>

* Move to Number:Intensity for solar

Signed-off-by: Matthew Skinner <matt@pcmus.com>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
This commit is contained in:
Matthew Skinner
2021-07-18 06:21:21 +10:00
committed by GitHub
parent b4a7c433f2
commit 8f3eef6ada
15 changed files with 1084 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.ipobserver-${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-ipobserver" description="IpObserver Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle dependency="true">mvn:org.jsoup/jsoup/1.8.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ipobserver/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2021 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.ipobserver.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link IpObserverBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class IpObserverBindingConstants {
public static final String BINDING_ID = "ipobserver";
public static final String REBOOT_URL = "/msgreboot.htm";
public static final String LIVE_DATA_URL = "/livedata.htm";
public static final String STATION_SETTINGS_URL = "/station.htm";
public static final int DISCOVERY_THREAD_POOL_SIZE = 15;
// List of all Thing Type UIDs
public static final ThingTypeUID THING_WEATHER_STATION = new ThingTypeUID(BINDING_ID, "weatherstation");
// List of all Channel ids
public static final String TEMP_INDOOR = "temperatureIndoor";
public static final String TEMP_OUTDOOR = "temperatureOutdoor";
public static final String INDOOR_HUMIDITY = "humidityIndoor";
public static final String OUTDOOR_HUMIDITY = "humidityOutdoor";
public static final String ABS_PRESSURE = "pressureAbsolute";
public static final String REL_PRESSURE = "pressureRelative";
public static final String WIND_DIRECTION = "windDirection";
public static final String WIND_AVERAGE_SPEED = "windAverageSpeed";
public static final String WIND_SPEED = "windSpeed";
public static final String WIND_GUST = "windGust";
public static final String WIND_MAX_GUST = "windMaxGust";
public static final String SOLAR_RADIATION = "solarRadiation";
public static final String UV = "uv";
public static final String UV_INDEX = "uvIndex";
public static final String HOURLY_RAIN_RATE = "rainHourlyRate";
public static final String DAILY_RAIN = "rainToday";
public static final String WEEKLY_RAIN = "rainForWeek";
public static final String MONTHLY_RAIN = "rainForMonth";
public static final String YEARLY_RAIN = "rainForYear";
public static final String INDOOR_BATTERY = "batteryIndoor";
public static final String OUTDOOR_BATTERY = "batteryOutdoor";
public static final String RESPONSE_TIME = "responseTime";
public static final String LAST_UPDATED_TIME = "lastUpdatedTime";
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2021 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.ipobserver.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link IpObserverConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class IpObserverConfiguration {
public String address = "";
public int pollTime = 20;
public int autoReboot = 2000;
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2021 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.ipobserver.internal;
import static org.openhab.binding.ipobserver.internal.IpObserverBindingConstants.LIVE_DATA_URL;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
/**
* The {@link IpObserverDiscoveryJob} class allows auto discovery of
* devices for a single IP address. This is used
* for threading to make discovery faster.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class IpObserverDiscoveryJob implements Runnable {
private IpObserverDiscoveryService discoveryClass;
private String ipAddress;
public IpObserverDiscoveryJob(IpObserverDiscoveryService service, String ip) {
this.discoveryClass = service;
this.ipAddress = ip;
}
@Override
public void run() {
if (isIpObserverDevice(this.ipAddress)) {
discoveryClass.submitDiscoveryResults(this.ipAddress);
}
}
private boolean isIpObserverDevice(String ip) {
Request request = discoveryClass.getHttpClient().newRequest("http://" + ip + LIVE_DATA_URL);
request.method(HttpMethod.GET).timeout(5, TimeUnit.SECONDS).header(HttpHeader.ACCEPT_ENCODING, "gzip");
ContentResponse contentResponse;
try {
contentResponse = request.send();
if (contentResponse.getStatus() == 200 && contentResponse.getContentAsString().contains("livedata.htm")) {
return true;
}
} catch (InterruptedException | TimeoutException | ExecutionException e) {
}
return false;
}
}

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2021 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.ipobserver.internal;
import static org.openhab.binding.ipobserver.internal.IpObserverBindingConstants.*;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
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 IpObserverDiscoveryService} is responsible for finding ipObserver devices.
*
* @author Matthew Skinner - Initial contribution.
*/
@Component(service = DiscoveryService.class, configurationPid = "discovery.ipobserver")
@NonNullByDefault
public class IpObserverDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_WEATHER_STATION);
private ExecutorService discoverySearchPool = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE);
private HttpClient httpClient;
@Activate
public IpObserverDiscoveryService(@Reference HttpClientFactory httpClientFactory) {
super(SUPPORTED_THING_TYPES_UIDS, 240);
httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_THING_TYPES_UIDS;
}
protected HttpClient getHttpClient() {
return httpClient;
}
public void submitDiscoveryResults(String ip) {
ThingUID thingUID = new ThingUID(THING_WEATHER_STATION, ip.replace('.', '_'));
HashMap<String, Object> properties = new HashMap<>();
properties.put("address", ip);
thingDiscovered(DiscoveryResultBuilder.create(thingUID).withProperties(properties).withLabel("Weather Station")
.withRepresentationProperty("address").build());
}
private void scanSingleSubnet(InterfaceAddress hostAddress) {
byte[] broadcastAddress = hostAddress.getBroadcast().getAddress();
// Create subnet mask from length
int shft = 0xffffffff << (32 - hostAddress.getNetworkPrefixLength());
byte oct1 = (byte) (((byte) ((shft & 0xff000000) >> 24)) & 0xff);
byte oct2 = (byte) (((byte) ((shft & 0x00ff0000) >> 16)) & 0xff);
byte oct3 = (byte) (((byte) ((shft & 0x0000ff00) >> 8)) & 0xff);
byte oct4 = (byte) (((byte) (shft & 0x000000ff)) & 0xff);
byte[] subnetMask = new byte[] { oct1, oct2, oct3, oct4 };
// calc first IP to start scanning from on this subnet
byte[] startAddress = new byte[4];
startAddress[0] = (byte) (broadcastAddress[0] & subnetMask[0]);
startAddress[1] = (byte) (broadcastAddress[1] & subnetMask[1]);
startAddress[2] = (byte) (broadcastAddress[2] & subnetMask[2]);
startAddress[3] = (byte) (broadcastAddress[3] & subnetMask[3]);
// Loop from start of subnet to the broadcast address.
for (int i = ByteBuffer.wrap(startAddress).getInt(); i < ByteBuffer.wrap(broadcastAddress).getInt(); i++) {
try {
InetAddress currentIP = InetAddress.getByAddress(ByteBuffer.allocate(4).putInt(i).array());
// Try to reach each IP with a timeout of 500ms which is enough for local network
if (currentIP.isReachable(500)) {
String host = currentIP.getHostAddress().toString();
logger.debug("Unknown device was found at: {}", host);
discoverySearchPool.execute(new IpObserverDiscoveryJob(this, host));
}
} catch (IOException e) {
}
}
}
@Override
protected void startScan() {
discoverySearchPool = Executors.newFixedThreadPool(DISCOVERY_THREAD_POOL_SIZE);
try {
ipAddressScan();
} catch (Exception exp) {
logger.debug("IpObserver discovery service encountered an error while scanning for devices: {}",
exp.getMessage());
}
}
@Override
protected void stopScan() {
discoverySearchPool.shutdown();
super.stopScan();
}
private void ipAddressScan() {
try {
for (Enumeration<NetworkInterface> enumNetworks = NetworkInterface.getNetworkInterfaces(); enumNetworks
.hasMoreElements();) {
NetworkInterface networkInterface = enumNetworks.nextElement();
List<InterfaceAddress> list = networkInterface.getInterfaceAddresses();
for (InterfaceAddress hostAddress : list) {
InetAddress inetAddress = hostAddress.getAddress();
if (!inetAddress.isLoopbackAddress() && inetAddress.isSiteLocalAddress()) {
logger.debug("Scanning all IP address's that IP {}/{} is on", hostAddress.getAddress(),
hostAddress.getNetworkPrefixLength());
scanSingleSubnet(hostAddress);
}
}
}
} catch (SocketException ex) {
}
}
}

View File

@@ -0,0 +1,348 @@
/**
* Copyright (c) 2010-2021 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.ipobserver.internal;
import static org.openhab.binding.ipobserver.internal.IpObserverBindingConstants.*;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.measure.Unit;
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.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openhab.core.library.types.DateTimeType;
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.library.unit.ImperialUnits;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Channel;
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.State;
import org.openhab.core.types.TypeParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link IpObserverHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Thomas Hentschel - Initial contribution.
* @author Matthew Skinner - Full re-write for BND, V3.0 and UOM
*/
@NonNullByDefault
public class IpObserverHandler extends BaseThingHandler {
private final HttpClient httpClient;
private final Logger logger = LoggerFactory.getLogger(IpObserverHandler.class);
private Map<String, ChannelHandler> channelHandlers = new HashMap<String, ChannelHandler>();
private @Nullable ScheduledFuture<?> pollingFuture = null;
private IpObserverConfiguration config = new IpObserverConfiguration();
// Config settings parsed from weather station.
private boolean imperialTemperature = false;
private boolean imperialRain = false;
// 0=lux, 1=w/m2, 2=fc
private String solarUnit = "0";
// 0=m/s, 1=km/h, 2=ft/s, 3=bft, 4=mph, 5=knot
private String windUnit = "0";
// 0=hpa, 1=inhg, 2=mmhg
private String pressureUnit = "0";
private class ChannelHandler {
private IpObserverHandler handler;
private Channel channel;
private String previousValue = "";
private Unit<?> unit;
private final ArrayList<Class<? extends State>> acceptedDataTypes = new ArrayList<Class<? extends State>>();
ChannelHandler(IpObserverHandler handler, Channel channel, Class<? extends State> acceptable, Unit<?> unit) {
super();
this.handler = handler;
this.channel = channel;
this.unit = unit;
acceptedDataTypes.add(acceptable);
}
public void processValue(String sensorValue) {
if (!sensorValue.equals(previousValue)) {
previousValue = sensorValue;
switch (channel.getUID().getId()) {
case LAST_UPDATED_TIME:
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm MM/dd/yyyy")
.withZone(TimeZone.getDefault().toZoneId());
ZonedDateTime zonedDateTime = ZonedDateTime.parse(sensorValue, formatter);
this.handler.updateState(this.channel.getUID(), new DateTimeType(zonedDateTime));
} catch (DateTimeParseException e) {
logger.debug("Could not parse {} as a valid dateTime", sensorValue);
}
return;
case INDOOR_BATTERY:
case OUTDOOR_BATTERY:
if ("1".equals(sensorValue)) {
handler.updateState(this.channel.getUID(), OnOffType.ON);
} else {
handler.updateState(this.channel.getUID(), OnOffType.OFF);
}
return;
}
State state = TypeParser.parseState(this.acceptedDataTypes, sensorValue);
if (state == null) {
return;
} else if (state instanceof QuantityType) {
handler.updateState(this.channel.getUID(),
QuantityType.valueOf(Double.parseDouble(sensorValue), unit));
} else {
handler.updateState(this.channel.getUID(), state);
}
}
}
}
public IpObserverHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
private void parseSettings(String html) {
Document doc = Jsoup.parse(html);
solarUnit = doc.select("select[name=unit_Solar] option[selected]").val();
windUnit = doc.select("select[name=unit_Wind] option[selected]").val();
pressureUnit = doc.select("select[name=unit_Pressure] option[selected]").val();
// 0=degC, 1=degF
if ("1".equals(doc.select("select[name=u_Temperature] option[selected]").val())) {
imperialTemperature = true;
} else {
imperialTemperature = false;
}
// 0=mm, 1=in
if ("1".equals(doc.select("select[name=u_Rainfall] option[selected]").val())) {
imperialRain = true;
} else {
imperialRain = false;
}
}
private void parseAndUpdate(String html) {
Document doc = Jsoup.parse(html);
String value = doc.select("select[name=inBattSta] option[selected]").val();
ChannelHandler localUpdater = channelHandlers.get("inBattSta");
if (localUpdater != null) {
localUpdater.processValue(value);
}
value = doc.select("select[name=outBattSta] option[selected]").val();
localUpdater = channelHandlers.get("outBattSta");
if (localUpdater != null) {
localUpdater.processValue(value);
}
Elements elements = doc.select("input");
for (Element element : elements) {
String elementName = element.attr("name");
value = element.attr("value");
if (!value.isEmpty()) {
logger.trace("Found element {}, value is {}", elementName, value);
localUpdater = channelHandlers.get(elementName);
if (localUpdater != null) {
localUpdater.processValue(value);
}
}
}
}
private void sendGetRequest(String url) {
Request request = httpClient.newRequest("http://" + config.address + url);
request.method(HttpMethod.GET).timeout(5, TimeUnit.SECONDS).header(HttpHeader.ACCEPT_ENCODING, "gzip");
String errorReason = "";
try {
long start = System.currentTimeMillis();
ContentResponse contentResponse = request.send();
if (contentResponse.getStatus() == 200) {
long responseTime = (System.currentTimeMillis() - start);
if (!this.getThing().getStatus().equals(ThingStatus.ONLINE)) {
updateStatus(ThingStatus.ONLINE);
logger.debug("Finding out which units of measurement the weather station is using.");
sendGetRequest(STATION_SETTINGS_URL);
}
if (url == STATION_SETTINGS_URL) {
parseSettings(contentResponse.getContentAsString());
setupChannels();
} else {
updateState(RESPONSE_TIME, new QuantityType<>(responseTime, MetricPrefix.MILLI(Units.SECOND)));
parseAndUpdate(contentResponse.getContentAsString());
}
if (config.autoReboot > 0 && responseTime > config.autoReboot) {
logger.debug("An Auto reboot of the IP Observer unit has been triggered as the response was {}ms.",
responseTime);
sendGetRequest(REBOOT_URL);
}
return;
} else {
errorReason = String.format("IpObserver request failed with %d: %s", contentResponse.getStatus(),
contentResponse.getReason());
}
} catch (TimeoutException e) {
errorReason = "TimeoutException: IpObserver was not reachable on your network";
} catch (ExecutionException e) {
errorReason = String.format("ExecutionException: %s", e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
errorReason = String.format("InterruptedException: %s", e.getMessage());
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorReason);
}
private void pollStation() {
sendGetRequest(LIVE_DATA_URL);
}
private void createChannelHandler(String chanName, Class<? extends State> type, Unit<?> unit, String htmlName) {
@Nullable
Channel channel = this.getThing().getChannel(chanName);
if (channel != null) {
channelHandlers.put(htmlName, new ChannelHandler(this, channel, type, unit));
}
}
private void setupChannels() {
if (imperialTemperature) {
logger.debug("Using imperial units of measurement for temperature.");
createChannelHandler(TEMP_INDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "inTemp");
createChannelHandler(TEMP_OUTDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "outTemp");
} else {
logger.debug("Using metric units of measurement for temperature.");
createChannelHandler(TEMP_INDOOR, QuantityType.class, SIUnits.CELSIUS, "inTemp");
createChannelHandler(TEMP_OUTDOOR, QuantityType.class, SIUnits.CELSIUS, "outTemp");
}
if (imperialRain) {
createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, ImperialUnits.INCH, "rainofhourly");
createChannelHandler(DAILY_RAIN, QuantityType.class, ImperialUnits.INCH, "rainofdaily");
createChannelHandler(WEEKLY_RAIN, QuantityType.class, ImperialUnits.INCH, "rainofweekly");
createChannelHandler(MONTHLY_RAIN, QuantityType.class, ImperialUnits.INCH, "rainofmonthly");
createChannelHandler(YEARLY_RAIN, QuantityType.class, ImperialUnits.INCH, "rainofyearly");
} else {
logger.debug("Using metric units of measurement for rain.");
createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE),
"rainofhourly");
createChannelHandler(DAILY_RAIN, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE), "rainofdaily");
createChannelHandler(WEEKLY_RAIN, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE), "rainofweekly");
createChannelHandler(MONTHLY_RAIN, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE), "rainofmonthly");
createChannelHandler(YEARLY_RAIN, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE), "rainofyearly");
}
if ("5".equals(windUnit)) {
createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, Units.KNOT, "avgwind");
createChannelHandler(WIND_SPEED, QuantityType.class, Units.KNOT, "windspeed");
createChannelHandler(WIND_GUST, QuantityType.class, Units.KNOT, "gustspeed");
createChannelHandler(WIND_MAX_GUST, QuantityType.class, Units.KNOT, "dailygust");
} else if ("4".equals(windUnit)) {
createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "avgwind");
createChannelHandler(WIND_SPEED, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "windspeed");
createChannelHandler(WIND_GUST, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "gustspeed");
createChannelHandler(WIND_MAX_GUST, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "dailygust");
} else if ("1".equals(windUnit)) {
createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, SIUnits.KILOMETRE_PER_HOUR, "avgwind");
createChannelHandler(WIND_SPEED, QuantityType.class, SIUnits.KILOMETRE_PER_HOUR, "windspeed");
createChannelHandler(WIND_GUST, QuantityType.class, SIUnits.KILOMETRE_PER_HOUR, "gustspeed");
createChannelHandler(WIND_MAX_GUST, QuantityType.class, SIUnits.KILOMETRE_PER_HOUR, "dailygust");
} else if ("0".equals(windUnit)) {
createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, Units.METRE_PER_SECOND, "avgwind");
createChannelHandler(WIND_SPEED, QuantityType.class, Units.METRE_PER_SECOND, "windspeed");
createChannelHandler(WIND_GUST, QuantityType.class, Units.METRE_PER_SECOND, "gustspeed");
createChannelHandler(WIND_MAX_GUST, QuantityType.class, Units.METRE_PER_SECOND, "dailygust");
} else {
logger.warn(
"The IP Observer is sending a wind format the binding does not support. Select one of the other units.");
}
if ("1".equals(solarUnit)) {
createChannelHandler(SOLAR_RADIATION, QuantityType.class, Units.IRRADIANCE, "solarrad");
} else if ("0".equals(solarUnit)) {
createChannelHandler(SOLAR_RADIATION, QuantityType.class, Units.LUX, "solarrad");
} else {
logger.warn(
"The IP Observer is sending fc (Foot Candles) for the solar radiation. Select one of the other units.");
}
if ("0".equals(pressureUnit)) {
createChannelHandler(ABS_PRESSURE, QuantityType.class, MetricPrefix.HECTO(SIUnits.PASCAL), "AbsPress");
createChannelHandler(REL_PRESSURE, QuantityType.class, MetricPrefix.HECTO(SIUnits.PASCAL), "RelPress");
} else if ("1".equals(pressureUnit)) {
createChannelHandler(ABS_PRESSURE, QuantityType.class, ImperialUnits.INCH_OF_MERCURY, "AbsPress");
createChannelHandler(REL_PRESSURE, QuantityType.class, ImperialUnits.INCH_OF_MERCURY, "RelPress");
} else if ("2".equals(pressureUnit)) {
createChannelHandler(ABS_PRESSURE, QuantityType.class, Units.MILLIMETRE_OF_MERCURY, "AbsPress");
createChannelHandler(REL_PRESSURE, QuantityType.class, Units.MILLIMETRE_OF_MERCURY, "RelPress");
}
createChannelHandler(WIND_DIRECTION, QuantityType.class, Units.DEGREE_ANGLE, "windir");
createChannelHandler(INDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "inHumi");
createChannelHandler(OUTDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "outHumi");
// The units for the following are ignored as they are not a QuantityType.class
createChannelHandler(UV, DecimalType.class, SIUnits.CELSIUS, "uv");
createChannelHandler(UV_INDEX, DecimalType.class, SIUnits.CELSIUS, "uvi");
// was outBattSta1 so some units may use this instead?
createChannelHandler(OUTDOOR_BATTERY, StringType.class, Units.PERCENT, "outBattSta");
createChannelHandler(OUTDOOR_BATTERY, StringType.class, Units.PERCENT, "outBattSta1");
createChannelHandler(INDOOR_BATTERY, StringType.class, Units.PERCENT, "inBattSta");
createChannelHandler(LAST_UPDATED_TIME, DateTimeType.class, SIUnits.CELSIUS, "CurrTime");
}
@Override
public void initialize() {
config = getConfigAs(IpObserverConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS);
}
@Override
public void dispose() {
channelHandlers.clear();
ScheduledFuture<?> localFuture = pollingFuture;
if (localFuture != null) {
localFuture.cancel(true);
localFuture = null;
}
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2021 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.ipobserver.internal;
import static org.openhab.binding.ipobserver.internal.IpObserverBindingConstants.THING_WEATHER_STATION;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
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 IpObserverHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.ipobserver", service = ThingHandlerFactory.class)
public class IpObserverHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_WEATHER_STATION);
protected final HttpClient httpClient;
@Activate
public IpObserverHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
protected HttpClient getHttpClient() {
return httpClient;
}
@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_WEATHER_STATION.equals(thingTypeUID)) {
return new IpObserverHandler(thing, httpClient);
}
return null;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="ipobserver" 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>IpObserver Binding</name>
<description>This is the binding for weather stations marketed under many brands that come with or have an IpObserver
station connected.</description>
</binding:binding>

View File

@@ -0,0 +1,249 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="ipobserver"
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="weatherstation">
<label>Weather Station</label>
<description>Use for any weather station sold under multiple brands that come with an IP Observer unit.</description>
<channels>
<channel id="temperatureIndoor" typeId="temperatureIndoor"/>
<channel id="temperatureOutdoor" typeId="system.outdoor-temperature"/>
<channel id="humidityIndoor" typeId="humidityIndoor"/>
<channel id="humidityOutdoor" typeId="system.atmospheric-humidity"/>
<channel id="pressureAbsolute" typeId="pressureAbsolute"/>
<channel id="pressureRelative" typeId="pressureRelative"/>
<channel id="windDirection" typeId="system.wind-direction"/>
<channel id="windAverageSpeed" typeId="windAverageSpeed"/>
<channel id="windSpeed" typeId="windSpeed"/>
<channel id="windGust" typeId="windGust"/>
<channel id="windMaxGust" typeId="windMaxGust"/>
<channel id="solarRadiation" typeId="solarRadiation"/>
<channel id="uv" typeId="uv"/>
<channel id="uvIndex" typeId="uvIndex"/>
<channel id="rainHourlyRate" typeId="rainHourlyRate"/>
<channel id="rainToday" typeId="rainToday"/>
<channel id="rainForWeek" typeId="rainForWeek"/>
<channel id="rainForMonth" typeId="rainForMonth"/>
<channel id="rainForYear" typeId="rainForYear"/>
<channel id="batteryOutdoor" typeId="system.low-battery"/>
<channel id="batteryIndoor" typeId="system.low-battery"/>
<channel id="responseTime" typeId="responseTime"/>
<channel id="lastUpdatedTime" typeId="lastUpdatedTime"/>
</channels>
<config-description>
<parameter name="address" type="text" required="true">
<context>network-address</context>
<label>Network Address</label>
<description>Hostname or IP for the IP Observer</description>
<default>192.168.1.243</default>
</parameter>
<parameter name="pollTime" type="integer" required="true" min="5" max="3600" unit="s">
<label>Poll Time</label>
<description>Time in seconds between each Scan of the livedata.htm from the ObserverIP</description>
<default>20</default>
</parameter>
<parameter name="autoReboot" type="integer" required="true" min="0" max="20000" unit="ms">
<label>Auto Reboot</label>
<description>Time in milliseconds to wait for a reply before rebooting the IP Observer. A value of 0 disables this
feature allowing you to manually trigger or use a rule to handle the reboots</description>
<default>2000</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="responseTime" advanced="true">
<item-type>Number:Time</item-type>
<label>Response Time</label>
<description>How many milliseconds it took to fetch the sensor readings from livedata.htm</description>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="temperatureIndoor">
<item-type>Number:Temperature</item-type>
<label>Indoor Temperature</label>
<description>Current Temperature Indoors</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="humidityIndoor">
<item-type>Number:Dimensionless</item-type>
<label>Indoor Humidity</label>
<description>Current Humidity Indoors</description>
<category>Humidity</category>
<tags>
<tag>Measurement</tag>
<tag>Humidity</tag>
</tags>
<state pattern="%.0f %%" readOnly="true"/>
</channel-type>
<channel-type id="pressureAbsolute">
<item-type>Number:Pressure</item-type>
<label>Pressure Absolute</label>
<description>Absolute Current Pressure</description>
<category>Pressure</category>
<tags>
<tag>Measurement</tag>
<tag>Pressure</tag>
</tags>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="pressureRelative">
<item-type>Number:Pressure</item-type>
<label>Pressure Relative</label>
<description>Relative Current Pressure</description>
<category>Pressure</category>
<tags>
<tag>Measurement</tag>
<tag>Pressure</tag>
</tags>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="solarRadiation">
<item-type>Number:Intensity</item-type>
<label>Solar Radiation</label>
<description>Solar Radiation</description>
<category>Sun</category>
<tags>
<tag>Measurement</tag>
<tag>Light</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="uv" advanced="true">
<item-type>Number</item-type>
<label>UV</label>
<description>UV</description>
<category>Sun</category>
<tags>
<tag>Measurement</tag>
<tag>Light</tag>
</tags>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="uvIndex" advanced="true">
<item-type>Number</item-type>
<label>UV Index</label>
<description>UV Index</description>
<category>Sun</category>
<tags>
<tag>Measurement</tag>
<tag>Light</tag>
</tags>
<state pattern="%.0f" readOnly="true"/>
</channel-type>
<channel-type id="windAverageSpeed">
<item-type>Number:Speed</item-type>
<label>Wind Average Speed</label>
<description>Average Wind Speed</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="windSpeed" advanced="true">
<item-type>Number:Speed</item-type>
<label>Wind Speed</label>
<description>Wind Speed</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="windGust" advanced="true">
<item-type>Number:Speed</item-type>
<label>Wind Gust</label>
<description>Wind Gust</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="windMaxGust" advanced="true">
<item-type>Number:Speed</item-type>
<label>Wind Max Gust</label>
<description>Max wind gust for today</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="rainHourlyRate">
<item-type>Number:Length</item-type>
<label>Rain Hourly Rate</label>
<description>How much rain will fall in an Hour if the rate continues</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state pattern="%.2f" readOnly="true"/>
</channel-type>
<channel-type id="rainToday">
<item-type>Number:Length</item-type>
<label>Rain Today</label>
<description>Rain since Midnight</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="rainForWeek" advanced="true">
<item-type>Number:Length</item-type>
<label>Rain for Week</label>
<description>Weekly Rain</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="rainForMonth" advanced="true">
<item-type>Number:Length</item-type>
<label>Rain for Month</label>
<description>Rain since 12:00 on the 1st of this month</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="rainForYear">
<item-type>Number:Length</item-type>
<label>Rain for Year</label>
<description>Total rain since 12:00 on 1st Jan</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="lastUpdatedTime" advanced="true">
<item-type>DateTime</item-type>
<label>Last Updated Time</label>
<description>Time of the last livedata scrape</description>
<category>Time</category>
<tags>
<tag>Measurement</tag>
<tag>Timestamp</tag>
</tags>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>