Files
lmwsip/lmwsip/__init__.py
Marcel Nijenhof da45361c0a
All checks were successful
continuous-integration/drone/push Build is passing
Omzetten naar python module
2020-09-04 17:50:46 +02:00

471 lines
15 KiB
Python

"""Module to support the LmwSip class
See: LmwSip"""
import socket
import ssl
import select
import time
import re
import logging
from datetime import datetime, timedelta
from dateutil import tz
class LmwSip:
"""Class to connect to the LMW Standard Interface prototcol (sip)
This class iplement connection to the Rijkswaterstaat Meetnet
Water (LMW) with the Standard Interface Protocol using the
Rijkswaterstaat Meetnet Infrastructuur (RMI).
https://waterberichtgeving.rws.nl/water-en-weer/metingen
Support for:
ti
cmd(wn, vw, as)
"""
def __init__(self, user=None, password=None,
host="sip-lmw.rws.nl", port=443, meetnet="LMW", ssl = True,
check_ssl = True, timeout = 10, log = None, cleartelnet = False):
"""LmwSip(user, password, [host], [port], [meetnet], [ssl], [check_ssl], [timeout], [log])
user(optinal): Lmw user name
password(optional): Lmw password
host(optional): Default sip-lmw.rws.nl
port(optional): Default 443
meetnet(optional): Default LMW
ssl(optional): Default true
check_ssl(optional): true
timeout(optional): 10
log(optional): None
cleartelnet: False
Opens the connection and logs in
"""
self.user = user
self.password = password
self.host = host
self.port = port
self.meetnet = meetnet
self.ssl = ssl
self.check_ssl = check_ssl
self.timeout = timeout
self._socket = None
self.cleartelnet = cleartelnet
if (log != None):
self.log = log
self.log.debug("LmwSip.init")
else:
try:
self.log = logging.getLogger("lmwsip")
self.log.debug("LmwSip.init: Start log")
except Exception as e:
print("Logger failed: %s" % e)
if (self.host != None):
self.connect()
if (self.user != None):
self.login()
def lasttime(self, parameter):
#
# Find the last valid 10 minute window.
# The measurement of 12:00 is avaiable at 12:05:30.
# Before 12:05:30 we should use 11:50:00.
#
# At 12:05:29 we substract 15:29 from the time!
#
# Also note that we use GMT. So we should add one hour
# because we use GMT +1 (MET, UTC-1)
#
if (parameter.find("10") != -1):
now=time.time()
dt = now%600
if (dt < 330):
now = 3000 + now - dt
else:
now = 3600 + now - dt
else:
#
# e.g. H1 use 30 seconds to calculate the time.
#
dt = now%600
if (dt < 30):
now = 3540 + now - dt
else:
now = 3600 + now - dt
time_of_day=time.strftime("%H:%M", time.gmtime(now))
return { "day": time.strftime("%d-%m-%Y", time.gmtime(now)),
"time_of_day": time.strftime("%H:%M", time.gmtime(now)) }
def connect(self):
"""connect()
connects to lmw with tcp using the values of the object creation.
"""
try:
self._tcp = socket.create_connection((self.host, self.port))
except Exception as e:
self.log.error("LmwSip.connect(%s, %s) failed: %s",
self.host, self.port, e)
raise LmwSipConnectError("LmwSip.connect: Socket create failed")
if (self.ssl):
try:
self._context = ssl.create_default_context()
self._context.check_hostname = self.check_ssl
self._ssl = self._context.wrap_socket(self._tcp,
server_hostname=self.host)
self._socket = self._ssl
except Exception as e:
self.log.error("LmwSip.connect setup ssl failed:\n%s", e)
raise LmwSipConnectError("LmwSip.connect: setup ssl failed")
else:
self._socket = self._tcp
self._socket.settimeout(self.timeout)
def closesocket(self):
"""Closes the socket and set the socket to None. Doesn't logout"""
try:
self.log.debug("LmwSip.closesocket")
self._socket.close()
except Exception as e:
pass
self._socket = None
def send(self, sipcmd):
"""send(sipcmd)
send a sip command to the server
"""
if self._socket != None:
try:
logcmd = sipcmd.strip('\r')
if re.match("^LI", logcmd, re.IGNORECASE):
logcmd = re.sub(",.*", ", ******", logcmd)
self.log.debug("LmwSip.send(%s)" % logcmd)
self._socket.sendall(sipcmd.encode('ascii'))
except Exception as e:
self.log.error("LmwSip.send(%s) failed: %s", sipcmd, e)
self.closesocket()
raise LmwSipConnectError("LmwSip.send: Socket connection lost")
else:
self.log.warning("LmwSip.send: No connection")
def recv(self):
"""recv()
recieve a answer from the sip server
"""
bytebuf=b''
stringbuf=""
while self._socket != None and re.search("\r$", bytebuf.decode('utf-8')) == None:
try:
self.log.debug("LmwSip.recv: Waiting for data");
bytebuf = self._socket.recv(4096)
if self.cleartelnet:
while bytebuf[0] == '\xff':
self.log.info("SipLmw.recv: telnet data: %s", bytebuf)
bytebuf = self._socket.recv(4096)
except Exception as e:
self.log.error("SipLmw.recv: socket timeout: %s", e)
self.closesocket()
raise LmwSipConnectError("LmwSip.recv: Socket timeout")
try:
stringbuf += bytebuf.decode('utf-8')
except Exception as e:
self.log.error("SipLmw.recv: decode error: %s", e)
self.closesocket()
raise LmwSipDecodeError("LmwSip.recv: decode error", bytebuf)
if self._socket == None:
self.log.warn("LmwSip.recv: No connection")
elif stringbuf[0] != '!':
self.log.warn("LmwSip.recv: Sip error: %s" % stringbuf.strip('\r'))
else:
self.log.debug("LmwSip.recv: result: %s" % stringbuf.strip('\r'))
return(stringbuf)
def login(self):
"""login()
Login lmw using the object creation user, password.
Raises a LmwLoginFailure exception on failure
"""
li="LI " + self.user + "," + self.password + "\r"
self.send(li)
d = self.recv()
if (d[0] != '!'):
raise LmwLoginFailure(self.user + ":" + d)
def ti(self):
"""ti()
Request the time from lmw and returns the string.
Raises a LmwCmdWarn of failure
"""
ti="TI " + self.meetnet + "\r"
self.send(ti)
d = self.recv()
return (d[2:-1])
def cmd(self, process, location, parameter, time_delta, day,
time_of_day, cmd_type="DATA"):
"""cmd(process, location, parameter, time_delta, day, time_of_day)
Send a cmd to LMW and returns the lmw string
process: <WN|VW|AS>
location: <lmw location (e.g. HOEK)>
parameter: <lmw parameter (e.g. H10)>
time_delta: <Time windows (max 23:59, e.g. +01:00>
day: <Date>
time_of_day: <Time>
cmd_type: [DATA|DATB|OORS|OORB|""]
Example:
lmw.cmd("WN", "HOEK", "H10", "+01:00", "13-08-2018", "16:00")
Returns:
The LMW answer string
"""
if (process == "AS"):
data=""
else:
data="," + cmd_type
cmdstr=process + " " + self.meetnet + "," + location + "," + \
parameter + "," + time_delta + "," + day + "," + \
time_of_day + data + "\r"
self.send(cmdstr)
d = self.recv()
if (d[0] != '!'):
raise LmwCmdWarn(cmdstr, d)
return (d[2:-1])
def value(self, process, location, parameter, day = None,
time_of_day = None):
"""value(process, location, parameter, [day], [time_of_day]):
Parameters:
process: <WN|VW|AS>
location: <lmw location (e.g. HOEK)>
parameter: <lmw parameter (e.g. H10)>
day: [date = now()]
time_of_day: [time = now()]
The default returns the last value.
Example:
lmw.data_string("WN", "HOEK", "H10")
Returns a single string value or None
"""
if (day == None or time_of_day == None):
last = self.lasttime(parameter)
if (day==None):
day=last["day"]
if (time_of_day==None):
time_of_day=last["time_of_day"]
res = self.cmd(process, location, parameter, "+00:00", day,
time_of_day, "DATA")
value=re.sub("/.*$", "", res)
if (value == "99999"):
value=""
elif (value == "-999999999"):
value=""
#
# We should check the "kwaliteit"
#
return(value)
def _lmwdelta_(self, window):
h = 24*window.days + window.seconds // 3600
m = (window.seconds % 3600)//60
return("+%02i:%02i" % (h, m))
def _roundtime_(self, time, parameter):
if time.microsecond != 0:
time += timedelta(microseconds=1000000-time.microsecond)
if time.second != 0:
time += timedelta(seconds=60-time.second)
if (parameter.find("10") != -1) and (time.minute % 10 != 0):
time += timedelta(minutes=(10-time.minute%10))
return(time)
def timeSerie(self, process, location, parameter,
startTime, endTime, cmd_type="DATB"):
"""timeSerie(process, location, parameter, startTime, endTime, cmd_type="DATA")
Parameters:
process: <WN|VW|AS>
location: <lmw location (e.g. HOEK)>
parameter: <lmw parameter (e.g. H10)>
startTime: Start time (datetime)
endTime: End time (datetime)
cmd_type: [DATA|DATB]
startTime is rounded up to the next measurement time.
So 12:00:00.000001 --> 12:00:10.00.0
The times should have correct timezone information. Otherwise local timezone
is assumed. Timezones are converted to 'GMT+1' for the sip commands.
Example:
lmw.data_string("WN", "HOEK", "H10", ...)
Returns a LmwtimeSerie object
Errors:
startTime > endTime
endTime - startTime > 24 hour
now - startTime < 30 days
"""
startTime = self._roundtime_(startTime.astimezone(tz.gettz('GMT+1')), parameter)
endTime = endTime.astimezone(tz.gettz('GMT+1'))
if (parameter.find("10") != -1):
delta = timedelta(minutes=10)
else:
delta = timedelta(minutes=1)
if startTime > endTime:
self.log.warn("starttime > endtime: %s > %s", startTime, endTime)
raise sipTimeSeriesError(startTime, endTime,
"starttime > endtime")
if datetime.now(tz=tz.gettz('GMT+1')) - startTime > timedelta(days=30):
self.log.warn("now() - starttime > 30 days: %s", startTime)
raise sipTimeSeriesError(startTime, endTime,
"now - starttime > 30 days")
self.log.debug("LmwSip.timeSerie: startTime: %s" % startTime)
self.log.debug("LmwSip.timeSerie: endTime: %s" % endTime)
if process == "VW":
cmd_type="DATA"
res = lmwTimeSerie(startTime, delta, "")
while startTime <= endTime:
if endTime - startTime > timedelta(days=1):
window = timedelta(days=1) - delta
else:
window = endTime-startTime
values = self.cmd(process, location, parameter,
self._lmwdelta_(window),
startTime.strftime("%d-%m-%Y"),
startTime.strftime("%H:%M"),
cmd_type)
res.addvalues(startTime, values)
startTime += window + delta
return(res)
def logout(self):
"""logout()
Logs of
"""
self.send("LO\r")
self.closesocket()
class lmwTimeSerie:
"""Class for lmw results.
The result are in lmwTimeSerie.ts as array
[ <time1>, [<value1 a, value1 b, ...], kwaliteit1, additionele kwaliteit1],
[ <time2>, [<value2 a, value2 b, ...], kwaliteit2, additionele kwaliteit2],
...
Note:
* For most measurements there is only one value (e.g H10).
* Additionale kwaliteit is optional and may contain None.
* Result times in UTC
"""
def __init__(self, start, delta, values=""):
"""lmwTimeSerie(start, delta, values)
Create a lmwTimeSerie object with:
start: Start time
delta: Period of the measurements
values: lmw result string
"""
self.ts = []
self.delta = delta
if values != "":
self.addvalues(start, values)
def addvalues(self, start, values):
"""addvalues(start, delta, values)
Add values to a timeserie
start: Start time
delta: Period of the measurements
values: lmw result string
"""
start = start.astimezone(tz.gettz('UTC'))
for e in values.split(";"):
v = e.split("/")
v[0] = v[0].split(",")
if len(v) == 2:
v.append(None)
self.ts.append([start, v[0], v[1], v[2]])
start += self.delta
class sipTimeSeriesError(Exception):
"""Parameter errors for timeSeries"""
def __init__(self, startTime, endTime, message):
self.startTime = startTime
self.endTime = endTime
self.message = message
def __str__(self):
return("%s\n starttime: %s\n end time: %s" %
(self.message, self.startTime, self.endTime))
class LmwSipConnectError(Exception):
"""Connection exceptions for LmwSip"""
def __init__(self, message):
self.message = message
def __str__(self):
return(self.message)
class LmwSipDecodeError(Exception):
"""Connection exceptions for LmwSip"""
def __init__(self, message, buf):
self.message = message
self.buf = buf
def __str__(self):
return(self.message + ":" + buf)
class LmwLoginFailure(Exception):
"""Exception from LmwSip on login failure"""
def __init__(self, user, message):
self.user = user
self.message = message
def __str__(self):
return("Login with user %s failed: %s" % (self.user, self.message))
class LmwCmdWarn(Warning):
"""Exception fro LmwSip on a cmd"""
def __init__(self, cmd, message):
self.cmd = cmd.replace('\r', '')
self.message = message
def __str__(self):
return("Cmd %s failed: %s" %(self.cmd, self.message))