Hi all - good you have been able to resolve the issue.
I just used the user agent from my work pc (windows 11 using chrome)
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Hi all - good you have been able to resolve the issue.
I just used the user agent from my work pc (windows 11 using chrome)
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Anyone having issues accessing this now?
I’ve been accessing this data using command_line: with success until recently. Before I move over to rest, I just want to check that it’s still working for others.
update - got it working with rest:
Adding my thanks to those who’ve alredy worked this through in the thread. My minor variation on a theme is to switch to timestamps as device class to get the automatic rendering as follows:
rest:
# BOM Tides
- resource: "https://www.bom.gov.au/australia/tides/scripts/getNextTides.php?aac=TAS_TP003&offset=false&tz=Australia%2FHobart"
scan_interval: 21600 # 6 hours
method: GET
headers:
User-Agent: Mozilla/5.0
Accept: application/json
sensor:
- name: Tide Sensor Last updated
value_template: "{{ value_json.results.current_time | timestamp_local }}"
device_class: timestamp
- name: Hobart High Tide Time
value_template: "{{ value_json.results.next_high.time | timestamp_local }}"
device_class: timestamp
- name: Hobart High Tide Height
value_template: "{{ value_json.results.next_high.height }}"
device_class: distance
unit_of_measurement: m
- name: Hobart Low Tide Time
value_template: "{{ value_json.results.next_low.time | timestamp_local }}"
device_class: timestamp
- name: Hobart Low Tide Height
value_template: "{{ value_json.results.next_low.height }}"
device_class: distance
unit_of_measurement: m
which I then threw into an entity card and added the ‘state’ as secondary objects to the times so I have both the actual time of the next low/high/last updated as well as the relative time to now
(What is it that most of the people working in this thread seem to be in Tassie
)
I just tried playing with your code - by adding it into configuration.yaml & changing the BOM code for my location. Unfortunately, I get “Unknown” as the rest.
Got it to work with further examples above, after spending an hour or so going down a vibe coding cesspit.
Question on the tidal data - seems like the BOM tidal information is pretty limited, only 3-4 locations in Sydney for example? The tidal data is out by an hour for high-tide and 30-minutes low-tide (I’m downstream of Botany Bay which is closest data point BOM provides). But I see WillyWeather has more accurate locational data, shame about the paid API.
I’ve come back to this to redo it. I decided to try WillyWeather as it has more locational data and the 5000 free pulls will probably last me a lifetime with him I implemented it.
Through a pyscript, I did a 365 day API pull, located it in a JSON file, then a second function to work out next high and low tide every 15 mins. It’s not super accurate (due to not wanting to pay for API pulls), you could be more accurate but for my purposes this was good enough.
On the dashboard front:
• apexcharts-card — draws the actual curve. Fed from a short list of upcoming (and one past, so the line connects smoothly through “now”) tide events, interpolated with a cosine smoothing function rather than straight lines between points, so it actually looks like a tide instead of a zigzag.
• stack-in-card — merges the chart and a details panel (next high/low, last synced, days of data remaining) into one visual block instead of two separate boxes.
• card-mod — for shrinking/dimming the details panel so it reads as secondary info under the main chart.
Location details replaced with placeholders — swap in your own before use.
YOUR_LOCATION_ID — your WillyWeather location ID (find via https://api.willyweather.com.au/v2/YOUR_API_KEY/search.json?query=YOUR_SUBURB)tide_ prefix rather than a place namesecrets.yamlwillyweather_api_key: "YOUR_API_KEY_HERE"
/config/pyscript/tide_sync.pyimport requests
import json
import yaml
from datetime import datetime
WW_LOCATION_ID = "YOUR_LOCATION_ID"
DATA_FILE = "/config/pyscript/tide_data.json"
@pyscript_compile
def _read_secrets():
with open("/config/secrets.yaml") as f:
return yaml.safe_load(f)
@pyscript_compile
def _fetch_tide_data(api_key, location_id):
url = f"https://api.willyweather.com.au/v2/{api_key}/locations/{location_id}/weather.json"
params = {"forecasts": "tides", "days": 365}
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
return response.json()
@pyscript_compile
def _write_data_file(data, path):
with open(path, "w") as f:
json.dump(data, f)
@pyscript_compile
def _read_data_file(path):
with open(path) as f:
return json.load(f)
@service
def tide_sync():
"""Pull a full year of tide predictions from WillyWeather and store locally."""
secrets = task.executor(_read_secrets)
api_key = secrets["willyweather_api_key"]
data = task.executor(_fetch_tide_data, api_key, WW_LOCATION_ID)
task.executor(_write_data_file, data, DATA_FILE)
entry_count = 0
for day in data["forecasts"]["tides"]["days"]:
entry_count += len(day["entries"])
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
pyscript.tide_sync_status = now_str
state.setattr("pyscript.tide_sync_status.entry_count", entry_count)
state.setattr("pyscript.tide_sync_status.issue_date_time", data["forecasts"]["tides"]["issueDateTime"])
log.info(f"Tide sync complete: {entry_count} entries saved.")
tide_refresh()
@service
def tide_refresh():
"""Recalculate the next high/low, an upcoming-events list, and days of data remaining. No API call."""
try:
data = task.executor(_read_data_file, DATA_FILE)
except FileNotFoundError:
log.warning("No stored tide data yet — run tide_sync first.")
return
now = datetime.now()
all_entries = []
for day in data["forecasts"]["tides"]["days"]:
for entry in day["entries"]:
all_entries.append(entry)
next_high = None
next_low = None
next_high_time = None
next_low_time = None
upcoming = []
last_past_entry = None
for entry in all_entries:
entry_time = datetime.strptime(entry["dateTime"], "%Y-%m-%d %H:%M:%S")
if entry_time <= now:
last_past_entry = entry
continue
if next_high is None and entry["type"] == "high":
next_high = entry
next_high_time = entry_time
if next_low is None and entry["type"] == "low":
next_low = entry
next_low_time = entry_time
if len(upcoming) < 6:
upcoming.append({
"dateTime": entry["dateTime"],
"height": entry["height"],
"type": entry["type"],
})
if next_high and next_low and len(upcoming) >= 6:
break
# Prepend the last past event so the chart curve connects smoothly through "now"
if last_past_entry:
upcoming.insert(0, {
"dateTime": last_past_entry["dateTime"],
"height": last_past_entry["height"],
"type": last_past_entry["type"],
})
# How many days of stored data are left before the next annual sync is needed
last_entry_time = datetime.strptime(all_entries[-1]["dateTime"], "%Y-%m-%d %H:%M:%S")
days_remaining = (last_entry_time - now).days
pyscript.tide_days_remaining = f"{days_remaining} days"
if next_high:
pyscript.tide_next_high = next_high_time.strftime("%I:%M %p")
state.setattr("pyscript.tide_next_high.height", next_high["height"])
state.setattr("pyscript.tide_next_high.timestamp", next_high["dateTime"])
if next_low:
pyscript.tide_next_low = next_low_time.strftime("%I:%M %p")
state.setattr("pyscript.tide_next_low.height", next_low["height"])
state.setattr("pyscript.tide_next_low.timestamp", next_low["dateTime"])
pyscript.tide_upcoming = len(upcoming)
state.setattr("pyscript.tide_upcoming.events", upcoming)
automations.yaml- id: tide_annual_sync
alias: "Tide - Annual Sync"
description: "Pulls a fresh year of tide predictions once a year, self-healing if a run is missed"
trigger:
- platform: time
at: "03:00:00"
- platform: homeassistant
event: start
condition:
- condition: template
value_template: >
{% set last = states('pyscript.tide_sync_status') %}
{{ last in ['unknown', 'unavailable', 'none', ''] or
(now() - strptime(last, '%Y-%m-%d %H:%M:%S')).days > 350 }}
action:
- service: pyscript.tide_sync
mode: single
- id: tide_refresh
alias: "Tide - Refresh Next Events"
description: "Recalculates next high/low from locally stored data every 15 minutes, no API call"
trigger:
- platform: time_pattern
minutes: "/15"
action:
- service: pyscript.tide_refresh
mode: single
Requires three HACS frontend cards: apexcharts-card, stack-in-card, card-mod.
type: custom:stack-in-card
cards:
- type: markdown
content: "## Tide"
- type: custom:apexcharts-card
header:
show: false
graph_span: 24h
span:
start: hour
offset: "-6h"
now:
show: true
label: Now
color: "#f2a541"
yaxis:
- min: 0
max: 2.2
decimals: 1
apex_config:
chart:
toolbar:
show: false
stroke:
curve: smooth
width: 3
fill:
type: gradient
gradient:
shadeIntensity: 1
opacityFrom: 0.35
opacityTo: 0.02
series:
- entity: pyscript.tide_upcoming
name: Tide Height
color: "#5b9bd5"
unit: m
type: area
data_generator: |
const events = entity.attributes.events;
const points = [];
const stepMinutes = 15;
for (let i = 0; i < events.length - 1; i++) {
const t0 = new Date(events[i].dateTime.replace(' ', 'T')).getTime();
const t1 = new Date(events[i + 1].dateTime.replace(' ', 'T')).getTime();
const h0 = events[i].height;
const h1 = events[i + 1].height;
const steps = Math.round((t1 - t0) / (stepMinutes * 60000));
for (let s = 0; s <= steps; s++) {
const frac = s / steps;
const smoothFrac = (1 - Math.cos(frac * Math.PI)) / 2;
const height = h0 + (h1 - h0) * smoothFrac;
const t = t0 + (t1 - t0) * frac;
points.push([t, Math.round(height * 100) / 100]);
}
}
return points;
- type: entities
show_header_toggle: false
entities:
- entity: pyscript.tide_next_high
name: Next High
icon: mdi:wave-arrow-up
- entity: pyscript.tide_next_low
name: Next Low
icon: mdi:wave-arrow-down
- entity: pyscript.tide_sync_status
name: Last Synced
icon: mdi:cloud-sync
- entity: pyscript.tide_days_remaining
name: Data Valid For
icon: mdi:calendar-clock
card_mod:
style: |
ha-card {
box-shadow: none;
background: transparent;
}
.card-content {
padding-top: 0px;
}
hui-generic-entity-row $: |
state-badge {
--mdc-icon-size: 16px;
width: 24px;
height: 24px;
}
.info {
font-size: 0.78em;
opacity: 0.75;
}
.info .secondary {
font-size: 0.7em;
}
.state {
font-size: 0.78em;
opacity: 0.75;
}