Simple ocean tides widget

Hi all. I found myself wanting to know the next few high and low tides at a particular location. I couldn’t find a shortcut or widget that did what I wanted, so I made one. I thought I’d post it for anyone else who might be interested.

I built it by modifying an existing widget that showed covid rates by country (which I had saved as “getWebText”). Unfortunately, I don’t have a record of who wrote it — but a big thank-you to them!

I’ve set up (and commented) a variety of parameters as variables defined up high so they’re easy to change, including the NOAA tide station you use, the colors and the font sizes. The URL to look up tide station identifiers is also in a comment.

Unfortunately, I think NOAA only provides U.S. tide predictions.

Here’s what it looks like:

Here’s the code for the widget — enjoy!

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-green; icon-glyph: user-md;

// by T. Francis (theofrancis@gmail.com)
// adapted from "getWebText" covid monitoring script/widget
// modify ==> items for easy customization

// ==> change "stationID and "stationName"
// to a station number and name from 
// https://www.tidesandcurrents.noaa.gov/tide_predictions.html

// examples: 
// • "8728958" "St. Joe Point"
// • "9451439" "Petersburg, Alaska"
const stationID = "8728958"
const stationName = "St. Joe Point"

// ==> set colors 
// background color
const bkgdColor = Color.black()
// tide colors
const highColor = Color.blue()
const lowColor = Color.green()
// heading colors
const flashColor = Color.white()
const stationColor = Color.blue()
const todayColor = Color.white()
const dateColor = Color.gray()

// ==> set font sizes
const flashSize = 10
const stationSize = 14
const dateSize = 11
const lineSize = 10

// ==> set rounding for tide heights
// (number of decimal places to round to)
const roundPlaces = 0

// ==> set test to true to use the test data while modifying the widget or app, to avoid hitting the API unnecessarily
var test = false;

// generate dates (defaults to today & tomorrow)
const today = new Date()
let currentDate = new Date();
let cDay = addLeadingZeros(currentDate.getDate())
let cMonth = addLeadingZeros(currentDate.getMonth() + 1)
let cYear = currentDate.getFullYear()
let cDateFmt = cYear+"-"+cMonth+"-"+cDay
let cDateTod = cYear+""+cMonth+""+cDay
let cDateTom = cYear+""+cMonth+""+(cDay+1)

// create api query url with station id and dates
const url = `https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?product=predictions&application=NOS.COOPS.TAC.WL&begin_date=${cDateTod}&end_date=${cDateTom}&datum=MLLW&station=${stationID}&time_zone=lst_ldt&units=english&interval=hilo&format=json`
console.log(url)

// load live data unless in test mode
if (test == false) {  
  const req = new Request(url)
  var res = await req.loadJSON()
}
else {
  res = {"predictions":[{"t":"2021-04-13 01:52","v":"0.797","type":"H"},{"t":"2021-04-13 04:01","v":"0.701","type":"L"},{"t":"2021-04-13 08:49","v":"1.156","type":"H"},{"t":"2021-04-13 17:31","v":"0.092","type":"L"},{"t":"2021-04-14 09:00","v":"1.308","type":"H"},{"t":"2021-04-14 18:24","v":"0.029","type":"L"}]}
}

// create widget or run within Scriptable

if (config.runsInWidget) {
  // create and show widget
  let widget = createWidget(stationName, createPredictions(), bkgdColor)
  Script.setWidget(widget)
  Script.complete()
} else {
  console.log(createPredictions())
  // make table
  let table = new UITable()
  
  // add header
  let row = new UITableRow()
  row.isHeader = true
  row.addText(`Next tides for ${stationName}`)
  table.addRow(row)
  
  // fill data
  for (p in res.predictions) {
    if(res.predictions[p]["type"]=="H"){
      var ttype = "High"
    }
    else {
      var ttype = "Low"  
    }
    console.log(res.predictions[p])
    table.addRow(createRow(ttype, res.predictions[p]["t"]))
  }

  if (config.runsWithSiri)
    Speech.speak(`The next ${res.predictions[0]["type"]} tide is at ${res.predictions[0]["t"]}.`)
  
  // present table
  table.present()
}

// create list of lists used by widget
function createPredictions() {
  var result = ""
  var datetime = ""
  var ddate = ""
  var height = ""
  lines = []
  // make components more user friendly
  for (p in res.predictions) {
    // H/L to High/Low (tide)
    if(res.predictions[p]["type"]=="H"){
      var ttype = "High"
    }
    else {
      var ttype = "Low"  
    }
    // split data and time
    datetime = res.predictions[p]["t"]
    ddate = datetime.split(" ")[0]
    // replace today's date with "Today" 
    if (ddate == cDateFmt) {
      ddate = "Today"
    }
    ttime = datetime.split(" ")[1]
    height = res.predictions[p]["v"]
    height = Math.round(height * (10^roundPlaces))/(10^roundPlaces)
    // create day's entry to pass to widget
    let line = [ddate,ttime,ttype,height]
    lines.push(line)
  }
  return lines
}

function createRow(title, datetime) {
  let row = new UITableRow()
  row.addText(datetime)
  row.addText(title)
  return row
}

function addLeadingZeros(n) {
  if (n <= 9) {
    return "0" + n;
  }
  return n
}


function createWidget(pretitle, lines, color) {
  let w = new ListWidget()
  w.backgroundColor = bkgdColor
  // add Tides header
  let flash = w.addText("Tides")
  flash.textColor = Color.white()
  flash.textOpacity = 1
  flash.font = Font.systemFont(10)
  // add station name 
  let preTxt = w.addText(pretitle)
  preTxt.textColor = Color.red()
  preTxt.textOpacity = 0.8
  preTxt.font = Font.systemFont(14)
  w.addSpacer(5)
  // add prediction lines
  var refDate = ""
  var txt = ""
  var dateHeader = ""
  for (line in lines) {
    if (lines[line][0] != refDate) {
      refDate = lines[line][0]
      dateHeader = w.addText(lines[line][0])
      if (lines[line][0] == "Today") {
        dateHeader.textColor = todayColor
      }
      else {
        dateHeader.textColor = Color.gray()
      }
      dateHeader.font = Font.systemFont(11)
    }
    let txt = "• " + lines[line][1] + ": " + lines[line][2] + " (" + lines[line][3] + " ft.)"
    let text = w.addText(txt)
    if (lines[line][2] == "High") {
      text.textColor = highColor
    }
    else {
      text.textColor = lowColor
    }  
    text.font = Font.systemFont(10)
  }
  return w
}
3 Likes

Awesome, thanks! Most of the tide apps out there are not great. This is a simple way to monitor a pre-defined station.

Glad it could be useful. It occurred to me after posting that it would be relatively easy to modify it so the station ID and name is passed vis the widget parameter. Then you could display several widgets on a Home Screen, each showing a specific ride station.

1 Like

Just landed on Scriptable and this widget is cool… even if i’m not close to surf spot :stuck_out_tongue:

1 Like

Great work! Looks like you put together a great widget

I get this error message when logging the response from the request:

{“error”:{“message”:" Wrong Date Format: Valid Date Format is yyyyMMdd, MM/dd/yyyy, yyyyMMdd HH:mm, or MM/dd/yyyy HH:mm"}}

Have you seen this error before?

Edit: I found a fix, for some reason it was treating the day as text and just adding a 1 to the end instead of incrementing the date.

One thing to think about too, is how this will work when it is the last day of the month.

Here is what I changed to make it work:

I’m not sure what changed. My widget seems to have stopped updating as well. Thanks for troubleshooting.

Happy to assist! Hopefully it stays working

I was looking for a similar widget for German coast but all the apps did not provide any.

So I did it with scriptable: GitHub - AlexBeep/scriptable-gezeiten: Two iOS-Widgets for Scriptable showing high/low tides for one spot at the present day (textual or graphical)

Unfortunately it wasn’t possible to use the “official” data and my source is less accurate.

The widget is taking data with WebView from a website.

Has anybody an idea how to get needed data from this page: https://tableau.bsh.de/views/Gezeitenvorausberechnung/Gezeitenvorausberechnung_Einzelpegel?Kurzname=Norddeich&Bezugsniveau=Seekartennull%20(SKN)&Zeiten=Gesetzliche%20Zeit%20(Sommer%3A%20MESZ%20%20%20Winter%3A%20MEZ)&%3Apixelratio=2&%3Aembed=y&%3AshowAppBanner=false&%3Adisplay_count=no&%3AshowVizHome=no&%3Aalerts=no&%3Atabs=no&%3AshowShareOptions=true&%3Atoolbar=yes

They are using HTML but some kind of database.

And no, unfortunately there is no API or anything.

1 Like

That is one complicated website for displaying just a simple table…

If you modify the URL, you might change the embed=y parameter to n – when I did that, I got more share sheet options; you might be better able to parse the underlying website markup and get information out of it.