Initial (working) version of waterconditions.py
This commit is contained in:
parent
21556d3a6f
commit
b24b8a1e4d
201
waterconditions.py
Executable file
201
waterconditions.py
Executable file
@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
#
|
||||||
|
# Description:
|
||||||
|
#
|
||||||
|
# Simple web server that retrieves data from waterdata.usgs.gov
|
||||||
|
# and outputs it in JSON format for use in Home Assistant.
|
||||||
|
# Designed to be used as a rest template.
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - Python 3.9+
|
||||||
|
# - requests
|
||||||
|
#
|
||||||
|
# Usage/installation:
|
||||||
|
#
|
||||||
|
# Takes a single argument: the 8-digit code identifying the station.
|
||||||
|
# This is part of the URL you'd use to view the information on the web,
|
||||||
|
# and is listed on the web page. For example,
|
||||||
|
# https://waterdata.usgs.gov/monitoring-location/14339000/
|
||||||
|
# is for measurement station 14339000.
|
||||||
|
# The web page title is:
|
||||||
|
# Rogue River at Dodge Bridge, Near Eagle Point, OR - 14339000
|
||||||
|
# This means that the command line would be:
|
||||||
|
# /path/to/binary/riverconditions.py 14339000
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Update the first line of this script to be the same python3 executable as
|
||||||
|
# your Home Assistant instance uses.
|
||||||
|
#
|
||||||
|
# To use the integration, add one or more river or lake sections to the sensor: section
|
||||||
|
# of your configuration.yaml file.
|
||||||
|
# ------------------------
|
||||||
|
# - name: rogue_river_curr
|
||||||
|
# platform: rest
|
||||||
|
# resource: 'http://192.168.1.4:8999/river-14339000'
|
||||||
|
# scan_interval: 1800
|
||||||
|
# json_attributes:
|
||||||
|
# - data
|
||||||
|
# value_template: 'Rogue River status at Dodge Bridge'
|
||||||
|
# - platform: template
|
||||||
|
# sensors:
|
||||||
|
# river_temp:
|
||||||
|
# friendly_name: "River temperature"
|
||||||
|
# device_class: temperature
|
||||||
|
# value_template: '{{ state_attr("sensor.rogue_river_curr", "data")["watertemp"] | round(0) }}'
|
||||||
|
# river_flow:
|
||||||
|
# friendly_name: "River flow rate"
|
||||||
|
# device_class: volume_flow_rate
|
||||||
|
# value_template: '{{ state_attr("sensor.rogue_river_curr", "data")["flow"] | round(0) }}'
|
||||||
|
# river_height:
|
||||||
|
# friendly_name: "River height"
|
||||||
|
# device_class: distance
|
||||||
|
# value_template: '{{ state_attr("sensor.rogue_river_curr", "data")["height"] | round(1) }}'
|
||||||
|
# - name: lostcreek_lake_curr
|
||||||
|
# platform: rest
|
||||||
|
# resource: 'http://192.168.1.4:8999/lake-14335040'
|
||||||
|
# scan_interval: 1800
|
||||||
|
# json_attributes:
|
||||||
|
# - data
|
||||||
|
# value_template: 'Lost Creek Lake status'
|
||||||
|
# - platform: template
|
||||||
|
# sensors:
|
||||||
|
# lake_level:
|
||||||
|
# friendly_name: "Lake level"
|
||||||
|
# device_class: distance
|
||||||
|
# ------------------------
|
||||||
|
# Values returned by the script are in native units, which means:
|
||||||
|
# * level: feet above sea level (lake)
|
||||||
|
# * flow: cubic feet per second (river)
|
||||||
|
# * watertemp: degrees Celsius (river)
|
||||||
|
# * height: feet (river, lake)
|
||||||
|
# Note that lakes may sometimes have both height and level. Height is a relative measurement,
|
||||||
|
# while level is an absolute (feet above sea level). It should always be the case
|
||||||
|
# that level-height for a lake is a constant (the zero point for the gauge).
|
||||||
|
#
|
||||||
|
# The URL should refer to the server and port on which you're running this script.
|
||||||
|
# The path for the URL must be either "lake-" or "river-" followed by the 8 digit number corresponding
|
||||||
|
# to the water sensor you want to query.
|
||||||
|
# You can find water sensors at https://waterdata.usgs.gov.
|
||||||
|
#
|
||||||
|
# You can use a regular Web browser to connect to this script; the page returned will contain the
|
||||||
|
# current values for your sensor in JSON format. This may be helpful in debugging your URL.
|
||||||
|
#
|
||||||
|
# You can use any value you want for name, but it must match the sensor specified in the value template.
|
||||||
|
# Similarly, you can name your river sensors anything you want.
|
||||||
|
#
|
||||||
|
# Scan interval should be relatively long, since the values aren't updated
|
||||||
|
# frequently. Minimum interval should be 600 seconds (every 10 minutes).
|
||||||
|
# However, since scan_interval doesn't always work, this web server will cache the retrieved values for you.
|
||||||
|
# It'll only query the USGS server if the retrieved value is at least request_interval seconds old.
|
||||||
|
# The default for this is 599, so the USGS server is only queried every 10 minutes. This keeps the
|
||||||
|
# load on the USGS server low, and prevents the USGS from banning you.
|
||||||
|
#
|
||||||
|
#==========================================================================
|
||||||
|
# Copyright 2025 Ethan L. Miller (code@ethanmiller.us)
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
|
#
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided with the distribution.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
import sys,re,time
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from http.server import *
|
||||||
|
|
||||||
|
request_interval = 599
|
||||||
|
global listen_port
|
||||||
|
listen_port = 8999
|
||||||
|
mappings = {
|
||||||
|
'river': {
|
||||||
|
'temperature': 'watertemp',
|
||||||
|
'streamflow': 'flow',
|
||||||
|
'height': 'height',
|
||||||
|
},
|
||||||
|
'lake': {
|
||||||
|
'surface elevation': 'level',
|
||||||
|
'height': 'height',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cached_requests = dict()
|
||||||
|
|
||||||
|
def get_conditions (station, station_type = 'river', n_tries = 4):
|
||||||
|
cur_time = time.time()
|
||||||
|
req = None
|
||||||
|
new_req = False
|
||||||
|
if station in cached_requests:
|
||||||
|
(req, req_time) = cached_requests[station]
|
||||||
|
if cur_time - req_time > request_interval:
|
||||||
|
req = None
|
||||||
|
else:
|
||||||
|
time_delta = cur_time - req_time
|
||||||
|
print (f'Reusing request for {station} {time_delta} seconds old')
|
||||||
|
if not req:
|
||||||
|
for i in range(n_tries):
|
||||||
|
try:
|
||||||
|
url = f'https://waterservices.usgs.gov/nwis/iv/?format=json&sites={station}&siteStatus=all'
|
||||||
|
req = requests.get (url, timeout=3)
|
||||||
|
if req.ok:
|
||||||
|
new_req = True
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
req = None
|
||||||
|
result = dict()
|
||||||
|
mp = mappings[station_type]
|
||||||
|
if req and req.ok:
|
||||||
|
j = req.json()
|
||||||
|
for v in j['value']['timeSeries']:
|
||||||
|
variable_name = v['variable']['variableName'].lower()
|
||||||
|
for k in mp.keys():
|
||||||
|
if k in variable_name:
|
||||||
|
result[mp[k]] = float(v['values'][0]['value'][0]['value'])
|
||||||
|
if new_req:
|
||||||
|
cached_requests[station] = (req, cur_time)
|
||||||
|
return result
|
||||||
|
|
||||||
|
class WaterConditionsHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET (self):
|
||||||
|
response_code = 404
|
||||||
|
data = 'Not found'
|
||||||
|
try:
|
||||||
|
m = re.search ('/(river|lake)-(\d+)', self.path)
|
||||||
|
station_type = m.group (1)
|
||||||
|
station = m.group (2)
|
||||||
|
result = get_conditions (station, station_type)
|
||||||
|
if result:
|
||||||
|
response_code = 200
|
||||||
|
data = json.dumps({'data': result})
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
response_code = 404
|
||||||
|
self.send_response (response_code)
|
||||||
|
self.send_header('content-type', 'text/plain')
|
||||||
|
self.end_headers ()
|
||||||
|
self.wfile.write (data.encode())
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
listen_port = int(sys.argv[1])
|
||||||
|
# print (get_conditions ('14339000', 'river'))
|
||||||
|
port = HTTPServer (('', listen_port), WaterConditionsHandler)
|
||||||
|
port.serve_forever ()
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user