-
Notifications
You must be signed in to change notification settings - Fork 18
ndbc-csv #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
ndbc-csv #190
Changes from 2 commits
d48bad3
7a4195f
465d940
1c593cc
16042ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| ### NDBC Buoy Data Driver- Last Updated 2026-03-25 | ||
|
|
||
| **_Company_**: Botts Innovative Research, Inc. | ||
| **_Developer_**: Tony Cook | ||
| **_Sensor Vendor_**: | ||
|
|
||
| OpenSensorhub driver for supporting data from the National Data Buoy Center network of Oceanicbuoys. It reads near-realtime data from CSV files on the NDBC Observations website located here: | ||
| https://www.ndbc.noaa.gov/data/latest_obs/latest_obs.txt | ||
|
|
||
| Documentation on Observation measurements, units, and methods of acquisition can be found here: | ||
| https://www.ndbc.noaa.gov/faq/measdes.shtml | ||
|
|
||
| The above links and method of access are subject to change. | ||
|
|
||
| ## Configuration | ||
|
|
||
| - **General:** (*Settings common to all OpenSensorHub drivers on the "General" tab.*) | ||
| - **Module ID:** *Not editable.* UUID automatically assigned by OpenSensorHub for this driver instance. | ||
| - **Module Class:** *Not editable.* The fully qualified name of the Java class implementing the drive | ||
| - **Module Name:** A name for the instance of the driver. Should be set to something short and human-readable that describes the upstream source of data, e.g. "Shout TS Trackers". This does not affect the operation of the driver, but is seen in user interfaces. | ||
| - **Description:** Any descriptive text that an administrator may want to enter for the benefit of users (or themselves). | ||
| - **SensorML URL:** URL to a SensorML description document for the driver or physical device the driver represents. Typically this is left blank, and OpenSensorHub will populate the SensorML description with sensible defaults. | ||
| - **Auto Start:** If checked, this driver will be started when OpenSensorHub starts. This should typically be checked for this driver. | ||
| - **Last Updated:** This should not be changed for this driver. (Though it could theoretically be used to indicate to clients that settings have changed, no known clients will use this value.) | ||
|
|
||
| - **BuoyConfig:** *(Settings specific to NDBC OpenSensorHub driver)* | ||
| - **realtimeUrl:** Serial Number/MAC ID for this BeastkitURL of realtime CSV data | ||
| - **pollingPeriod:** Polling Period in ms | ||
|
|
||
| --- | ||
|
|
||
| ## Outputs | ||
|
|
||
| #### BuoyOutput | ||
| * timestamp - Julian1970 timestamp of the measurement | ||
| * id - NDBC buoy ID | ||
| * latitude - latitude of buoy location (decimal degrees) | ||
| * longitude - longitude of buoy location in (decimal degrees) | ||
| * altitude? - should this be there? | ||
| * windSpeed - average wind speed (m/s) | ||
| * windDir - wind direction (degrees) | ||
| * windGust - peak wind gust (m/s) | ||
| * significantWaveHeight - (meters) | ||
| * dominantWavePeriod - (seconds) | ||
| * averageWavePeriod - (seconds) | ||
| * waveDirection - (degrees) | ||
| * seaLevelPressure - (hPa) | ||
| * pressureTendency - (hPa/?) | ||
| * airTemperature - (degrees C) | ||
| * waterTemperature - (degrees C) | ||
| * dewPoint - (degrees C) | ||
| * visibility - (miles) | ||
| * tideWaterLevel - (feet relative to Mean Lower Level Water) | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| description = 'NDBC Buoy Data Archive' | ||
| ext.details = 'Connector to retrieve historical observations from NDBC datasets' | ||
| version = '0.2.0' | ||
|
|
||
| dependencies { | ||
| implementation 'org.sensorhub:sensorhub-core:' + oshCoreVersion | ||
| embeddedImpl 'org.jsoup:jsoup:1.11.2' | ||
| } | ||
|
|
||
| // add info to OSGi manifest | ||
| osgi { | ||
| manifest { | ||
| attributes('Bundle-Vendor': 'Botts Innovative Research Inc.') | ||
| attributes('Bundle-Activator': 'org.sensorhub.impl.ndbc.Activator') | ||
| } | ||
| } | ||
|
|
||
| // add info to maven pom | ||
| ext.pom >>= { | ||
| developers { | ||
| developer { | ||
| id 'theRestOfMe' | ||
| name 'Tony Cook' | ||
| organization 'Botts Innovative Research Inc.' | ||
| organizationUrl 'https://www.opensensorhub.org' | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /***************************** BEGIN LICENSE BLOCK *************************** | ||
|
|
||
| The contents of this file are subject to the Mozilla Public License, v. 2.0. | ||
| If a copy of the MPL was not distributed with this file, You can obtain one | ||
| at http://mozilla.org/MPL/2.0/. | ||
|
|
||
| Software distributed under the License is distributed on an "AS IS" basis, | ||
| WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | ||
| for the specific language governing rights and limitations under the License. | ||
|
|
||
| Copyright (C) 2021 Sensia Software LLC. All Rights Reserved. | ||
|
|
||
| ******************************* END LICENSE BLOCK ***************************/ | ||
|
|
||
| package org.sensorhub.impl.sensor.ndbc; | ||
|
|
||
| import org.osgi.framework.BundleActivator; | ||
| import org.sensorhub.utils.OshBundleActivator; | ||
|
|
||
|
|
||
| /* | ||
| * Needed to expose java services as OSGi services | ||
| */ | ||
| public class Activator extends OshBundleActivator implements BundleActivator | ||
| { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| /***************************** BEGIN LICENSE BLOCK *************************** | ||
|
|
||
| The contents of this file are subject to the Mozilla Public License, v. 2.0. | ||
| If a copy of the MPL was not distributed with this file, You can obtain one | ||
| at http://mozilla.org/MPL/2.0/. | ||
|
|
||
| Software distributed under the License is distributed on an "AS IS" basis, | ||
| WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | ||
| for the specific language governing rights and limitations under the License. | ||
|
|
||
| Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. | ||
|
|
||
| ******************************* END LICENSE BLOCK ***************************/ | ||
|
|
||
| package org.sensorhub.impl.sensor.ndbc; | ||
|
|
||
| import org.sensorhub.api.config.DisplayInfo; | ||
| import org.sensorhub.api.sensor.SensorConfig; | ||
|
|
||
|
|
||
| public class BuoyConfig extends SensorConfig | ||
| { | ||
| @DisplayInfo(desc="URL of realtime CSV data downloads") | ||
| String realtimeUrl = "https://www.ndbc.noaa.gov/data/latest_obs/latest_obs.txt"; | ||
|
|
||
| @DisplayInfo(desc="Polling Period in ms") | ||
| long pollingPeriod = 60_000L; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| package org.sensorhub.impl.sensor.ndbc; | ||
|
|
||
| import java.io.BufferedReader; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.InputStreamReader; | ||
| import java.net.URL; | ||
| import java.time.LocalDateTime; | ||
| import java.time.ZoneId; | ||
| import java.time.ZonedDateTime; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * | ||
| */ | ||
|
|
||
| public class BuoyDataReader | ||
| { | ||
| static final String MISSING = "MM"; | ||
| static final Logger logger = LoggerFactory.getLogger(BuoyDataReader.class); | ||
|
|
||
| public static List<BuoyDataRecord> read(String url) throws IOException { | ||
| List<BuoyDataRecord> recs = new ArrayList<>(); | ||
| URL dataUrl = new URL(url); | ||
|
|
||
| try (BufferedReader reader = new BufferedReader(new InputStreamReader(dataUrl.openStream()))) { | ||
| // Skip Header lines | ||
| String header = reader.readLine(); | ||
| boolean headerOk = checkHeader(header); | ||
| if(!headerOk) { | ||
| throw new IOException("Unexpected header format: " + header); | ||
| } | ||
| String units = reader.readLine(); | ||
| boolean unitsOk = checkUnits(units); | ||
| if(!unitsOk) { | ||
| throw new IOException("Unexpected units format: " + units); | ||
| } | ||
| while(true) { | ||
| String inline = reader.readLine(); | ||
| try { | ||
| if(inline == null) break; | ||
| if(inline.isBlank()) continue; | ||
|
|
||
| String [] values = inline.split("\\s+"); | ||
| // #STN LAT LON YYYY MM DD hh mm WDIR WSPD GST WVHT DPD APD MWD PRES PTDY ATMP WTMP DEWP VIS TIDE | ||
| // #text deg deg yr mo day hr mn degT m/s m/s m sec sec degT hPa hPa degC degC degC nmi ft | ||
| BuoyDataRecord rec = new BuoyDataRecord(); | ||
| rec.id = values[0]; | ||
| rec.lat = parseDouble(values[1]); | ||
| rec.lon = parseDouble(values[2]); | ||
| // time | ||
| Integer year = parseInt(values[3]); | ||
| Integer month = parseInt(values[4]); | ||
| Integer day = parseInt(values[5]); | ||
| Integer hour = parseInt(values[6]); | ||
| Integer minute = parseInt(values[7]); | ||
| LocalDateTime ldt = LocalDateTime.of(year, month, day, hour, minute); | ||
| ZonedDateTime zdt = ldt.atZone(ZoneId.of("GMT")); | ||
| rec.timeMs = zdt.toInstant().toEpochMilli(); | ||
| rec.windDir = parseInt(values[8]); | ||
| rec.windSpeed = parseDouble(values[9]); | ||
| rec.windGust = parseDouble(values[10]); | ||
| rec.wvht = parseDouble(values[11]); | ||
| rec.dpd = parseDouble(values[12]); | ||
| rec.apd = parseDouble(values[13]); | ||
| rec.mwd = parseDouble(values[14]); | ||
| rec.pressure = parseDouble(values[15]); | ||
| rec.ptdy = parseDouble(values[16]); | ||
| rec.airTemp = parseDouble(values[17]); | ||
| rec.waterTemp = parseDouble(values[18]); | ||
| rec.dewPt = parseDouble(values[19]); | ||
| rec.visibility = parseDouble(values[20]); | ||
| rec.tide = parseDouble(values[21]); | ||
| recs.add(rec); | ||
| } catch (Exception e) { | ||
| // If any exception, skip this record and continue | ||
| logger.error("Error parsing line: {}" , inline, e); | ||
| } | ||
| } | ||
|
|
||
| return recs; | ||
| } catch(Exception e) { | ||
| throw new IOException("Error reading data file: " + url, e); | ||
| } | ||
| } | ||
|
|
||
| // #STN LAT LON YYYY MM DD hh mm WDIR WSPD GST WVHT DPD APD MWD PRES PTDY ATMP WTMP DEWP VIS TIDE | ||
| public static boolean checkHeader(String header) { | ||
| // TODO check fields are as expected | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is a planned enhancement to the module, make sure we log it in the backlog with enough detail to come in here and complete. |
||
| String [] fields = header.split("\\s+"); | ||
| return (fields.length == 22); | ||
| } | ||
|
|
||
| // #text deg deg yr mo day hr mn degT m/s m/s m sec sec degT hPa hPa degC degC degC nmi ft | ||
| public static boolean checkUnits(String units) { | ||
| String [] fields = units.split("\\s+"); | ||
| // TODO check units are as expected | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is a planned enhancement to the module, make sure we log it in the backlog with enough detail to come in here and complete. |
||
| return (fields.length == 22); | ||
| } | ||
|
|
||
| // Default URL is https://www.ndbc.noaa.gov/data/latest_obs/latest_obs.txt | ||
| public static boolean testObsFileIsReachable(String url) throws IOException { | ||
| URL dataUrl = new URL(url); | ||
| try(InputStream is = dataUrl.openStream()) { | ||
| return true; | ||
| } catch (IOException e) { | ||
| logger.error("URL {} is not reachable" , url, e); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| public static Double parseDouble(String s) { | ||
| s = s.trim(); | ||
| if(s.isEmpty()) | ||
| return null; | ||
| if(MISSING.equals(s)) { | ||
| return null; | ||
| } | ||
| try { | ||
| return Double.parseDouble(s); | ||
| } catch (NumberFormatException e) { | ||
| logger.error("", e); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty log messasge |
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| public static Integer parseInt(String s) { | ||
| s = s.trim(); | ||
| if(s.isEmpty()) | ||
| return null; | ||
| if(MISSING.equals(s)) { | ||
| return null; | ||
| } | ||
| try { | ||
| return Integer.parseInt(s); | ||
| } catch (NumberFormatException e) { | ||
| logger.error("", e); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty log message |
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| public static void testReader() throws IOException { | ||
| String dataFile = "https://www.ndbc.noaa.gov/data/latest_obs/latest_obs.txt"; | ||
| logger.debug("Reading data from {}", dataFile); | ||
| List<BuoyDataRecord> recs = read(dataFile); | ||
| for(BuoyDataRecord rec: recs) { | ||
| logger.debug(rec.toString()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| /***************************** BEGIN LICENSE BLOCK *************************** | ||
| The contents of this file are subject to the Mozilla Public License, v. 2.0. | ||
| If a copy of the MPL was not distributed with this file, You can obtain one | ||
| at http://mozilla.org/MPL/2.0/. | ||
|
|
||
| Software distributed under the License is distributed on an "AS IS" basis, | ||
| WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | ||
| for the specific language governing rights and limitations under the License. | ||
|
|
||
| Copyright (C) 2023 Botts Innovative Research, Inc. All Rights Reserved. | ||
| ******************************* END LICENSE BLOCK ***************************/ | ||
| package org.sensorhub.impl.sensor.ndbc; | ||
|
|
||
| /** | ||
| * | ||
| * @author tcook | ||
| * | ||
| */ | ||
|
|
||
| public class BuoyDataRecord | ||
| { | ||
| // #STN LAT LON YYYY MM DD hh mm WDIR WSPD GST WVHT DPD APD MWD PRES PTDY ATMP WTMP DEWP VIS TIDE | ||
| // #text deg deg yr mo day hr mn degT m/s m/s m sec sec degT hPa hPa degC degC degC nmi f | ||
| String id; | ||
| Double lat; | ||
| Double lon; | ||
| long timeMs; // Both Realtime and Historical files show times in UTC only. See the Acquisition Time help topic for a more detailed description of observation times. | ||
| Integer windDir; // Wind direction (the direction the wind is coming from in degrees clockwise from true N) during the same period used for WSPD. | ||
| Double windSpeed; // Wind speed (m/s) averaged over an eight-minute period for buoys and a two-minute period for land stations. Reported Hourly. | ||
| Double windGust; // Peak 5 or 8 second gust speed (m/s) measured during the eight-minute or two-minute period. The 5 or 8 second period can be determined by payload | ||
| Double wvht; // Significant wave height (meters) is calculated as the average of the highest one-third of all of the wave heights during the 20-minute sampling period. | ||
| Double dpd; // Dominant wave period (seconds) is the period with the maximum wave energy. | ||
| Double apd; // Average wave period (seconds) of all waves during the 20-minute period | ||
| Double mwd; // The direction from which the waves at the dominant period (DPD) are coming. The units are degrees from true North, increasing clockwise, with North as 0 (zero) degrees and East as 90 degrees. | ||
| Double pressure; // Sea level pressure (hPa). For C-MAN sites and Great Lakes buoys, the recorded pressure is reduced to sea level using the method described in NWS Technical Procedures Bulletin 291 (11/14/80). | ||
| Double ptdy; // Pressure Tendency is the direction (plus or minus) and the amount of pressure change (hPa)for a three hour period ending at the time of observation. (not in Historical files) | ||
| Double airTemp; // Air temperature (Celsius). For sensor heights on buoys, see Hull Descriptions. For sensor heights at C-MAN stations, see C-MAN Sensor Locations | ||
| Double waterTemp; // Sea surface temperature (Celsius). For buoys the depth is referenced to the hull's waterline. For fixed platforms it varies with tide, but is referenced to, or near Mean Lower Low Water (MLLW). | ||
| Double dewPt; // Dewpoint temperature taken at the same height as the air temperature measurement. | ||
| Double visibility; // Station visibility (nautical miles). Note that buoy stations are limited to reports from 0 to 1.6 nmi. | ||
| Double tide; // The water level in feet above or below Mean Lower Low Water (MLLW). | ||
|
|
||
| Double getTimeMs() { | ||
| return -1.0; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| StringBuilder b = new StringBuilder(); | ||
| b.append(id + ","); | ||
| b.append(lat + ","); | ||
| b.append(lon + ","); | ||
| b.append(windDir + ","); | ||
| b.append(windSpeed + ","); | ||
| b.append(windGust + ","); | ||
| b.append(pressure + ","); | ||
| b.append(airTemp + ","); | ||
| b.append(waterTemp + ","); | ||
| b.append(dewPt); | ||
|
|
||
| return b.toString(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /***************************** BEGIN LICENSE BLOCK *************************** | ||
|
|
||
| The contents of this file are subject to the Mozilla Public License, v. 2.0. | ||
| If a copy of the MPL was not distributed with this file, You can obtain one | ||
| at http://mozilla.org/MPL/2.0/. | ||
|
|
||
| Software distributed under the License is distributed on an "AS IS" basis, | ||
| WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | ||
| for the specific language governing rights and limitations under the License. | ||
|
|
||
| Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. | ||
|
|
||
| ******************************* END LICENSE BLOCK ***************************/ | ||
|
|
||
| package org.sensorhub.impl.sensor.ndbc; | ||
|
|
||
| import org.sensorhub.api.module.IModule; | ||
| import org.sensorhub.api.module.IModuleProvider; | ||
| import org.sensorhub.api.module.ModuleConfig; | ||
| import org.sensorhub.impl.module.JarModuleProvider; | ||
|
|
||
|
|
||
| public class BuoyDescriptor extends JarModuleProvider implements IModuleProvider | ||
| { | ||
| @Override | ||
| public Class<? extends IModule<?>> getModuleClass() | ||
| { | ||
| return BuoySensor.class; | ||
| } | ||
|
|
||
|
|
||
| @Override | ||
| public Class<? extends ModuleConfig> getModuleConfigClass() | ||
| { | ||
| return BuoyConfig.class; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bundle Activator attribute value must follow the package structure of the Activator file,
org.sensorhub.impl.sensor.ndbc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will be corrected in next push