diff --git a/weather.cmd b/weather.cmd new file mode 100755 index 0000000..d624617 --- /dev/null +++ b/weather.cmd @@ -0,0 +1,14 @@ +#!./bin/linux-x86/softIocPy + +epicsEnvSet("PYTHONPATH", "${PWD}/python") + +dbLoadDatabase("dbd/softIocPy.dbd") +softIocPy_registerRecordDeviceDriver(pdbbase) + +dbLoadRecords("db/weather.db","P=kisp:,LOC=KISP") +#dbLoadRecords("db/weather.db","P=khwv:,LOC=KHWV") +#dbLoadRecords("db/weather.db","P=unnt:,LOC=UNNT") + +iocInit() + +dbl > weather.dbl diff --git a/weatherApp/Makefile b/weatherApp/Makefile new file mode 100644 index 0000000..01fc5af --- /dev/null +++ b/weatherApp/Makefile @@ -0,0 +1,24 @@ +TOP=.. +include $(TOP)/configure/CONFIG +#---------------------------------------- +# ADD MACRO DEFINITIONS AFTER THIS LINE + +#---------------------------------------------------- +# Optimization of db files using dbst (DEFAULT: NO) +#DB_OPT = YES + +#---------------------------------------------------- +# Create and install (or just install) into /db +# databases, templates, substitutions like this +DB += weather.db +PY += weather.py + +#---------------------------------------------------- +# If .db template is not named *.template add +# _template = + +include $(TOP)/configure/RULES +include $(TOP)/configure/RULES_PY +#---------------------------------------- +# ADD RULES AFTER THIS LINE + diff --git a/weatherApp/weather.db b/weatherApp/weather.db new file mode 100644 index 0000000..8e4f549 --- /dev/null +++ b/weatherApp/weather.db @@ -0,0 +1,106 @@ +record(stringin, "$(P)Wthr-I") { + field(DTYP, "Python Device") + field(DESC, "Station ID") + field(INP , "@weather $(LOC) showID") + field(PINI, "YES") +} + +record(ai, "$(P)K:Update-I") { + field(DTYP, "Python Device") + field(DESC, "Update period") + field(INP , "@weather $(LOC) updatePeriod") + field(SCAN, "I/O Intr") + field(EGU , "min") + field(HOPR, "30") + field(LOPR, "0") +} + + +record(stringin, "$(P)K-I") { + field(DTYP, "Python Device") + field(DESC, "Measurement time") + field(INP , "@weather $(LOC) getISOTime") + field(SCAN, "I/O Intr") + field(TSE , "-2") +} + +record(ai, "$(P)T-I") { + field(DTYP, "Python Device") + field(DESC, "Air temperature") + field(INP , "@weather $(LOC) getTemperatureCelsius") + field(SCAN, "I/O Intr") + field(TSE , "-2") + field(EGU , "C") + field(HOPR, "30") + field(LOPR, "-5") + field(PREC, "1") +} +record(ai, "$(P)T:Chill-I") { + field(DTYP, "Python Device") + field(DESC, "Wind chill") + field(INP , "@weather $(LOC) getWindchill") + field(SCAN, "I/O Intr") + field(TSE , "-2") + field(EGU , "C") + field(HOPR, "30") + field(LOPR, "-15") + field(PREC, "1") +} + + +record(ai, "$(P)Humid-I") { + field(DTYP, "Python Device") + field(DESC, "Relative humidity") + field(INP , "@weather $(LOC) getHumidity") + field(SCAN, "I/O Intr") + field(TSE , "-2") + field(EGU , "%") + field(HOPR, "100") + field(LOPR, "0") +} + +record(ai, "$(P)P-I") { + field(DTYP, "Python Device") + field(INP , "@weather $(LOC) getPressure") + field(SCAN, "I/O Intr") + field(TSE , "-2") + field(EGU , "mbar") + field(HOPR, "1100") + field(LOPR, "900") +} + +record(stringin, "$(P)Cond-I") { + field(DTYP, "Python Device") + field(DESC, "Sky condition") + field(INP , "@weather $(LOC) getSkyConditions") + field(SCAN, "I/O Intr") + field(TSE , "-2") +} +record(stringin, "$(P)Wthr-I") { + field(DTYP, "Python Device") + field(DESC, "Current Weather") + field(INP , "@weather $(LOC) getWeather") + field(SCAN, "I/O Intr") + field(TSE , "-2") +} + +record(ai, "$(P)Dir:Wind-I") { + field(DTYP, "Python Device") + field(DESC, "Wind direction") + field(INP , "@weather $(LOC) getWindDirection") + field(SCAN, "I/O Intr") + field(TSE , "-2") + field(EGU , "deg") + field(HOPR, "360") + field(LOPR, "0") +} +record(ai, "$(P)V:Wind-I") { + field(DTYP, "Python Device") + field(DESC, "Wind speed") + field(INP , "@weather $(LOC) getWindSpeed") + field(SCAN, "I/O Intr") + field(TSE , "-2") + field(EGU , "m/s") + field(HOPR, "20") + field(LOPR, "0") +} diff --git a/weatherApp/weather.py b/weatherApp/weather.py new file mode 100644 index 0000000..7d07605 --- /dev/null +++ b/weatherApp/weather.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +from __future__ import print_function + +import socket, time + +from weakref import WeakValueDictionary + +from devsup.hooks import addHook +from devsup.util import StoppableThread +from devsup.db import IOScanListBlock + +try: + import pymetar +except ImportError: + print("The pymetar package could not be imported!") + raise + +_isofmt = '%Y-%m-%d %H:%M:%SZ' + +def iso2sec(str): + """Convert the string returned by pymetar to posix time. + """ + rtime = time.mktime(time.strptime(str, _isofmt)) + return rtime - (time.altzone if time.daylight else time.timezone) + +_stations = WeakValueDictionary() + +def getStation(name): + try: + return _stations[name] + except KeyError: + S = ReportScanner(name) + _stations[name] = S + return S + +class DataWatcher(object): + """Weather watcher device support. + + field(INP,"weather KISP getTemperatureCelsius") + of + field(INP,"weather KISP showID") + """ + + # disable automatic RVAL -> VAL conversion. + # We will update VAL ourselves. + raw = True + + def __init__(self, rec, args): + station, name = args.split(None, 1) + + self.staid = station + self.sta = getStation(station) + self.attr = name + self.last = None + + if name.startswith('get'): + # Assume this is a method of pymetar.WeatherReport + self.allowScan = self.sta.scan.add + self.process = self.process_report + else: + # Our own internal info + self.allowScan = self.sta.intscan.add + self.process = getattr(self, name) + try: + rec.UDF = 0 + except AttributeError: + pass + + def detach(self, rec): + pass + + def showID(self, rec, report): + rec.VAL = self.staid + + def updatePeriod(self, rec, report): + rec.VAL = self.sta.updatePeriod/60.0 + + def process_report(self, rec, report): + if report is not None: + self.last = report + else: + report = self.last + + if report is None: + rec.setSevr() + return + + fn = getattr(self.last, self.attr) + newval = fn() + try: + rec.VAL = newval + except ValueError: + rec.setSevr() + try: + rec.UDF = 0 + except AttributeError: + pass + if rec.TSE==-2: + rec.setTime(report._updatetime) + +build = DataWatcher + +class ReportScanner(StoppableThread): + """Driver thread which occasionally polls for new metar data + """ + + def __init__(self, station): + self.io = pymetar.ReportFetcher() + self.station = station + self.initPeriod = self.updatePeriod = 15*60.0 + self.minPeriod = 10*60.0 + self.maxPeriod = 2*60*60.0 + self.lastUpdate = None # Time of last report + self.scan = IOScanListBlock() # I/O Intr scan for report data + self.intscan = IOScanListBlock() # scan for internal info + + super(ReportScanner,self).__init__() + + addHook('AfterIocRunning', self.start) + addHook('AtIocExit', self.join) + + def run(self): + print("Starting scanner for ",self.station) + + while self.shouldRun(): + try: + report = self.io.FetchReport(self.station) + pymetar.ReportParser().ParseReport(report) + + rtime = iso2sec(report.getISOTime()) + report._updatetime = rtime + print('update',report.getISOTime(),rtime,self.lastUpdate) + + if self.lastUpdate is not None: + if self.lastUpdate >= rtime: + #print('No update, Wait a little longer next time') + self.updatePeriod = self.initPeriod + + else: # self.lastUpdate < rtime + #print('Got an update') + self.updatePeriod = rtime - self.lastUpdate + self.updatePeriod = max(self.minPeriod, min(self.updatePeriod, self.maxPeriod)) + self.updatePeriod += 15*60.0 + self.scan.interrupt(reason=report) + else: + self.scan.interrupt(reason=report) + + self.lastUpdate = rtime + + except socket.error, e: + print("download error for",self.station,":",e) + self.updatePeriod = self.initPeriod + + + self.intscan.interrupt() + + #print('Waiting',self.updatePeriod) + self.sleep(self.updatePeriod) + + print('Done')