diff --git a/README.md b/README.md index 28104ea..cc14def 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Depending on if/how you have java installed, you may be able to just double-clic If not, run with ``` -java -jar weatherlink.jar +java -jar weatherlink.jar [ip address] ``` If you don't have java, grab the latest 11 version for your platform: https://adoptopenjdk.net/releases.html?variant=openjdk11&jvmVariant=hotspot @@ -21,6 +21,8 @@ https://adoptopenjdk.net/releases.html?variant=openjdk11&jvmVariant=hotspot # First Run It should be able to auto-locate your WeatherLinkLive, so long as it is on the local network. +If it fails to find your Weather Link Live, you can set the IP address as a command line parameter after the jar file name. + When you first run it, things may be a bit sparse. Data will fill in as it runs. # Data Store @@ -42,6 +44,6 @@ There are lots of TODOs.... useful things I may add (pull requests welcome) # Release Notes ``` -mvn -B gitflow:release-start gitflow:release-finish -DreleaseVersion=1.04 -DdevelopmentVersion=1.05 +mvn -B gitflow:release-start gitflow:release-finish -DreleaseVersion=1.05 -DdevelopmentVersion=1.06 ``` diff --git a/pom.xml b/pom.xml index a524adb..b7fb8f0 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ net.sagebits.weatherlink weatherlink - 1.04 + 1.05 jar WeatherLink Logger and GUI @@ -90,15 +90,9 @@ 11.2 - net.straylightlabs + net.sagebits hola - 0.2.1 - - - ch.qos.logback - logback-classic - - + 0.2.4-sagebits diff --git a/src/main/java/net/sagebits/weatherlink/data/DataFetcher.java b/src/main/java/net/sagebits/weatherlink/data/DataFetcher.java index fa54330..59e0b6f 100644 --- a/src/main/java/net/sagebits/weatherlink/data/DataFetcher.java +++ b/src/main/java/net/sagebits/weatherlink/data/DataFetcher.java @@ -3,7 +3,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map.Entry; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.LogManager; @@ -48,9 +47,7 @@ public WeatherProperty getDataFor(String wllDeviceId, String sensorId, StoredDat { ConcurrentHashMap data = mostRecentData.computeIfAbsent(wllDeviceId + "|" + sensorId, keyAgain -> new ConcurrentHashMap<>()); - log.debug("Data requested for {} from {} {}", sdt, wllDeviceId, sensorId); - - return data.computeIfAbsent(sdt, keyAgain -> { + WeatherProperty wp = data.computeIfAbsent(sdt, keyAgain -> { //we don't yet have data we are tracking for this element. See if we have any data to populate with.... //Try to read it from the DB @@ -64,6 +61,9 @@ public WeatherProperty getDataFor(String wllDeviceId, String sensorId, StoredDat return readData; }); + log.debug("Data requested for {} from {} {}, returning {}", sdt, wllDeviceId, sensorId, wp.get()); + return wp; + } /** diff --git a/src/main/java/net/sagebits/weatherlink/data/DataReader.java b/src/main/java/net/sagebits/weatherlink/data/DataReader.java index b370095..49c5959 100644 --- a/src/main/java/net/sagebits/weatherlink/data/DataReader.java +++ b/src/main/java/net/sagebits/weatherlink/data/DataReader.java @@ -20,6 +20,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import javafx.application.Platform; +import javafx.beans.property.LongProperty; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.SimpleLongProperty; import net.sagebits.weatherlink.data.live.LiveDataListener; import net.sagebits.weatherlink.data.periodic.PeriodicData; import net.straylightlabs.hola.dns.Domain; @@ -48,6 +52,8 @@ public class DataReader private final JsonFactory factory = new JsonFactory(); private final ObjectMapper mapper = new ObjectMapper(factory); + + private final LongProperty lastReadAttemptTime = new SimpleLongProperty(System.currentTimeMillis()); /** * @param wllAddress The IP address of the WeatherLinkLive. If not provided, attempts to auto-discover. This constructor will fail if it cannot be @@ -148,7 +154,7 @@ public Thread newThread(Runnable r) } catch (Exception e) { - log.error("Request for live data failed:", e); + log.error("Request for live data failed:" + e); } }, 0, 1 ,TimeUnit.HOURS); @@ -158,7 +164,7 @@ public Thread newThread(Runnable r) log.error("Problem setting up for live data", e); } } - periodicTask = timed.scheduleAtFixedRate(() -> readData(), 0, this.pollInterval,TimeUnit.SECONDS); + periodicTask = timed.scheduleAtFixedRate(() -> readData(), 1, this.pollInterval,TimeUnit.SECONDS); } @@ -166,6 +172,7 @@ private void readData() { try { + Platform.runLater(() -> lastReadAttemptTime.set(System.currentTimeMillis())); String data = readBytes(new URL("http://" + address + ":" + port + "/v1/current_conditions")); log.trace("Periodic Data: {}", data); @@ -180,7 +187,8 @@ private void readData() } catch (Exception e) { - log.warn("Error during periodic data read, delaying and rescheduling", e); + //Don't need a stack trace here + log.warn("Error during periodic data read, delaying and rescheduling: {}", e.toString()); //Its probably busy. Lets sleep for a bit, and give it time to recover. //Will do this by canceling our current task, and rescheduling after a delay. ScheduledExecutorService localRef = timed; @@ -223,14 +231,19 @@ public void stopReading() } } + public ReadOnlyLongProperty getLastReadAttemptTime() + { + return lastReadAttemptTime; + } + public static String readBytes(URL url) throws IOException { InputStream is = null; try { URLConnection con = url.openConnection(); - con.setReadTimeout(1000); - con.setConnectTimeout(250); + con.setReadTimeout(2000); + con.setConnectTimeout(1000); con.connect(); is = con.getInputStream(); return new String(is.readAllBytes(), StandardCharsets.UTF_8); diff --git a/src/main/java/net/sagebits/weatherlink/data/WeatherProperty.java b/src/main/java/net/sagebits/weatherlink/data/WeatherProperty.java index e3f1bc8..4b008d6 100644 --- a/src/main/java/net/sagebits/weatherlink/data/WeatherProperty.java +++ b/src/main/java/net/sagebits/weatherlink/data/WeatherProperty.java @@ -56,11 +56,20 @@ public void setTimeStamp(long timeStamp) } /** - * Will return the bound timestamp, if bound + * Returns the bound, (live) timestamp, if present and valid and up-to-date, otherwise, returns the local (database) timestamp. */ public long getTimeStamp() { - return boundTo == null ? timeStamp : boundTo.getTimeStamp(); + if (boundTo == null || boundTo.asDouble().get() == -100.0) + { + return getLocalTimeStamp(); + } + if ((this.timeStamp - 8000) > boundTo.timeStamp) + { + //We don't seem to be getting live updates, return the newer stored data. + return getLocalTimeStamp(); + } + return boundTo.getTimeStamp(); } public long getLocalTimeStamp() @@ -104,6 +113,10 @@ public void unbind() boundTo = null; } + /** + * Returns the bound, (live) data, if present and valid and up-to-date, otherwise, returns the local (database) data. + * @see javafx.beans.property.ObjectPropertyBase#get() + */ @Override public Object get() { diff --git a/src/main/java/net/sagebits/weatherlink/data/live/LiveData.java b/src/main/java/net/sagebits/weatherlink/data/live/LiveData.java index 5f5b479..d1ef17c 100644 --- a/src/main/java/net/sagebits/weatherlink/data/live/LiveData.java +++ b/src/main/java/net/sagebits/weatherlink/data/live/LiveData.java @@ -10,6 +10,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import javafx.application.Platform; +import javafx.beans.property.LongProperty; +import javafx.beans.property.ReadOnlyLongProperty; +import javafx.beans.property.SimpleLongProperty; /** * Instance class to hold the most current live data at any given time. Use this to get to a set of @@ -26,6 +30,8 @@ public class LiveData //Map weatherLinkLive instances (by their did) to map of condition data (one per sensor id 'lsid') private ConcurrentHashMap> liveData_= new ConcurrentHashMap<>(2); + private LongProperty lastLiveData = new SimpleLongProperty(0); + private LiveData() { //singleton @@ -62,12 +68,18 @@ public ConditionsLive getLiveData(String weatherLinkLiveId, String sensorId) return conditions.computeIfAbsent(sensorId, keyAgain -> new ConditionsLive(sensorId, null)); } + public ReadOnlyLongProperty getLastDataTime() + { + return lastLiveData; + } + protected void update(JsonNode data) { String did = Optional.ofNullable(data.get("did")).orElseThrow().asText(); ConcurrentHashMap conditions = liveData_.computeIfAbsent(did, keyAgain -> new ConcurrentHashMap<>(2)); final long ts = Long.parseLong(Optional.ofNullable(data.get("ts")).orElseThrow().asText()) * 1000; + Platform.runLater(() -> lastLiveData.set(ts)); ArrayNode conditionsData = Optional.ofNullable((ArrayNode) data.get("conditions")).orElseThrow(); Iterator condition = conditionsData.elements(); diff --git a/src/main/java/net/sagebits/weatherlink/gui/WeatherLauncher.java b/src/main/java/net/sagebits/weatherlink/gui/WeatherLauncher.java index dda7e17..81dd259 100644 --- a/src/main/java/net/sagebits/weatherlink/gui/WeatherLauncher.java +++ b/src/main/java/net/sagebits/weatherlink/gui/WeatherLauncher.java @@ -1,11 +1,29 @@ package net.sagebits.weatherlink.gui; +import java.util.regex.Pattern; +import org.apache.logging.log4j.LogManager; import javafx.application.Application; public class WeatherLauncher { public static void main(String[] args) { + if (args != null && args.length > 0) + { + Pattern p = Pattern.compile("^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"); + if (p.matcher(args[0]).matches()) + { + WeatherLinkLiveGUIController.ip = args[0]; + LogManager.getLogger(WeatherLauncher.class).debug("Read IP '{}' from command line arg", args[0]); + } + else + { + LogManager.getLogger(WeatherLauncher.class).debug("Passed in param '{}' not an ip, ignoring.", args[0]); + } + } Application.launch(WeatherLinkLiveGUI.class); } } diff --git a/src/main/java/net/sagebits/weatherlink/gui/WeatherLinkLiveGUI.java b/src/main/java/net/sagebits/weatherlink/gui/WeatherLinkLiveGUI.java index c202892..64bfc47 100644 --- a/src/main/java/net/sagebits/weatherlink/gui/WeatherLinkLiveGUI.java +++ b/src/main/java/net/sagebits/weatherlink/gui/WeatherLinkLiveGUI.java @@ -20,7 +20,6 @@ public class WeatherLinkLiveGUI extends Application private static Stage mainStage_; private WeatherLinkLiveGUIController wllc_; - public static Logger logger = LogManager.getLogger(WeatherLinkLiveGUI.class); @Override diff --git a/src/main/java/net/sagebits/weatherlink/gui/WeatherLinkLiveGUIController.java b/src/main/java/net/sagebits/weatherlink/gui/WeatherLinkLiveGUIController.java index 02201be..e2ad9eb 100644 --- a/src/main/java/net/sagebits/weatherlink/gui/WeatherLinkLiveGUIController.java +++ b/src/main/java/net/sagebits/weatherlink/gui/WeatherLinkLiveGUIController.java @@ -18,8 +18,10 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.function.DoubleSupplier; import java.util.function.Supplier; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.math3.util.Precision; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -66,6 +68,7 @@ import net.sagebits.weatherlink.data.DataReader; import net.sagebits.weatherlink.data.StoredDataTypes; import net.sagebits.weatherlink.data.WeatherProperty; +import net.sagebits.weatherlink.data.live.LiveData; import net.sagebits.weatherlink.data.periodic.PeriodicData; import net.sagebits.weatherlink.gui.gapchart.GapLineChart; import net.sagebits.weatherlink.gui.gapchart.GapNumberAxis; @@ -89,6 +92,7 @@ public class WeatherLinkLiveGUIController private ArrayList> midnightTasks = new ArrayList<>(); private ScheduledExecutorService periodicJobs = Executors.newScheduledThreadPool(2, r -> new Thread(r, "Periodic GUI Jobs")); + protected static String ip = null; @FXML void initialize() @@ -110,7 +114,7 @@ public void finishInit(Stage mainStage) log.debug("Data Reader init thread starts"); try { - dr = new DataReader(Optional.empty()); + dr = new DataReader(Optional.ofNullable(StringUtils.isBlank(ip) ? null : ip)); dr.startReading(10, true); } catch (Exception e) @@ -477,7 +481,7 @@ private Gauge buildQuarterWindGauge(String wllDeviceId, String sensorOutdoorId) .markers(dayMax, tenMH, twoMH, tenMA, twoMA, oneMA) .markersVisible(true) .knobType(KnobType.STANDARD) - .knobColor(Gauge.DARK_COLOR) + .knobColor(Color.RED) .needleShape(NeedleShape.FLAT) .needleType(NeedleType.VARIOMETER) .needleSize(NeedleSize.THIN) @@ -540,7 +544,46 @@ private Gauge buildQuarterWindGauge(String wllDeviceId, String sensorOutdoorId) tenMA.valueProperty().bind(DataFetcher.getInstance().getDataFor(wllDeviceId, sensorOutdoorId, StoredDataTypes.wind_speed_avg_last_10_min).asDouble()); twoMA.valueProperty().bind(DataFetcher.getInstance().getDataFor(wllDeviceId, sensorOutdoorId, StoredDataTypes.wind_speed_avg_last_2_min).asDouble()); oneMA.valueProperty().bind(DataFetcher.getInstance().getDataFor(wllDeviceId, sensorOutdoorId, StoredDataTypes.wind_speed_avg_last_1_min).asDouble()); + + Tooltip tt = new Tooltip("Mode Pending"); + Tooltip.install(gauge, tt); + final SimpleDateFormat sdf = new SimpleDateFormat("h:mm:ss"); + + final Consumer updateTooltip = input -> + { + //This will give us a pulse, every read attempt. Don't actually care about the value. + if (currentWind.getTimeStamp() < (System.currentTimeMillis() - 6000)) + { + //More than 6 seconds out of date, missed at least 2 live data pulses. + gauge.setKnobColor(Color.BLACK); + tt.setText("Poll Last Update " + sdf.format(new Date(currentWind.getTimeStamp()))); + } + else if (currentWind.getTimeStamp() < (System.currentTimeMillis() - 30000)) + { + //More than 30 seconds for any data. + gauge.setKnobColor(Color.RED); + tt.setText("Last Update " + sdf.format(new Date(currentWind.getTimeStamp()))); + } + else + { + gauge.setKnobColor(Color.GREEN); + tt.setText("Live Last Update " + sdf.format(new Date(currentWind.getTimeStamp()))); + } + }; + if (dr != null) + { + dr.getLastReadAttemptTime().addListener((value, old, newv) -> + { + updateTooltip.accept(null); + }); + } + + LiveData.getInstance().getLastDataTime().addListener((value, old, newv) -> + { + updateTooltip.accept(null); + }); + return gauge; } @@ -1206,7 +1249,10 @@ public void shutdown() try { periodicJobs.shutdownNow(); - dr.stopReading(); + if (dr != null) + { + dr.stopReading(); + } } catch (Exception e) {