542 lines
19 KiB
Plaintext
Executable File
542 lines
19 KiB
Plaintext
Executable File
#!/usr/bin/env seiscomp-python
|
|
# -*- coding: utf-8 -*-
|
|
############################################################################
|
|
# Copyright (C) GFZ Potsdam #
|
|
# All rights reserved. #
|
|
# #
|
|
# GNU Affero General Public License Usage #
|
|
# This file may be used under the terms of the GNU Affero #
|
|
# Public License version 3.0 as published by the Free Software Foundation #
|
|
# and appearing in the file LICENSE included in the packaging of this #
|
|
# file. Please review the following information to ensure the GNU Affero #
|
|
# Public License version 3.0 requirements will be met: #
|
|
# https://www.gnu.org/licenses/agpl-3.0.html. #
|
|
############################################################################
|
|
|
|
import sys
|
|
import subprocess
|
|
import traceback
|
|
|
|
from seiscomp import client, core, datamodel, logging, seismology, system, math
|
|
|
|
|
|
class VoiceAlert(client.Application):
|
|
|
|
def __init__(self, argc, argv):
|
|
client.Application.__init__(self, argc, argv)
|
|
|
|
self.setMessagingEnabled(True)
|
|
self.setDatabaseEnabled(True, True)
|
|
self.setLoadRegionsEnabled(True)
|
|
self.setMessagingUsername("")
|
|
self.setPrimaryMessagingGroup(client.Protocol.LISTENER_GROUP)
|
|
self.addMessagingSubscription("EVENT")
|
|
self.addMessagingSubscription("LOCATION")
|
|
self.addMessagingSubscription("MAGNITUDE")
|
|
|
|
self.setAutoApplyNotifierEnabled(True)
|
|
self.setInterpretNotifierEnabled(True)
|
|
|
|
self.setLoadCitiesEnabled(True)
|
|
self.setLoadRegionsEnabled(True)
|
|
|
|
self._ampType = "snr"
|
|
self._citiesMaxDist = 20
|
|
self._citiesMinPopulation = 50000
|
|
|
|
self._cache = None
|
|
self._eventDescriptionPattern = None
|
|
self._ampScript = None
|
|
self._alertScript = None
|
|
self._eventScript = None
|
|
|
|
self._ampProc = None
|
|
self._alertProc = None
|
|
self._eventProc = None
|
|
|
|
self._newWhenFirstSeen = False
|
|
self._prevMessage = {}
|
|
self._agencyIDs = []
|
|
|
|
def createCommandLineDescription(self):
|
|
self.commandline().addOption(
|
|
"Generic",
|
|
"first-new",
|
|
"calls an event a new event when it is " "seen the first time",
|
|
)
|
|
self.commandline().addGroup("Alert")
|
|
self.commandline().addStringOption(
|
|
"Alert",
|
|
"amp-type",
|
|
"specify the amplitude type to listen to",
|
|
self._ampType,
|
|
)
|
|
self.commandline().addStringOption(
|
|
"Alert",
|
|
"amp-script",
|
|
"specify the script to be called when a "
|
|
"stationamplitude arrived, network-, stationcode and amplitude are "
|
|
"passed as parameters $1, $2 and $3",
|
|
)
|
|
self.commandline().addStringOption(
|
|
"Alert",
|
|
"alert-script",
|
|
"specify the script to be called when a "
|
|
"preliminary origin arrived, latitude and longitude are passed as "
|
|
"parameters $1 and $2",
|
|
)
|
|
self.commandline().addStringOption(
|
|
"Alert",
|
|
"event-script",
|
|
"specify the script to be called when an "
|
|
"event has been declared; the message string, a flag (1=new event, "
|
|
"0=update event), the EventID, the arrival count and the magnitude "
|
|
"(optional when set) are passed as parameter $1, $2, $3, $4 and $5",
|
|
)
|
|
self.commandline().addGroup("Cities")
|
|
self.commandline().addStringOption(
|
|
"Cities",
|
|
"max-dist",
|
|
"maximum distance for using the distance " "from a city to the earthquake",
|
|
)
|
|
self.commandline().addStringOption(
|
|
"Cities",
|
|
"min-population",
|
|
"minimum population for a city to " "become a point of interest",
|
|
)
|
|
self.commandline().addGroup("Debug")
|
|
self.commandline().addStringOption("Debug", "eventid,E", "specify Event ID")
|
|
return True
|
|
|
|
def init(self):
|
|
if not client.Application.init(self):
|
|
return False
|
|
|
|
try:
|
|
self._newWhenFirstSeen = self.configGetBool("firstNew")
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
agencyIDs = self.configGetStrings("agencyIDs")
|
|
for item in agencyIDs:
|
|
item = item.strip()
|
|
if item not in self._agencyIDs:
|
|
self._agencyIDs.append(item)
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
if self.commandline().hasOption("first-new"):
|
|
self._newWhenFirstSeen = True
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
self._eventDescriptionPattern = self.configGetString("poi.message")
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
self._citiesMaxDist = self.configGetDouble("poi.maxDist")
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
self._citiesMaxDist = self.commandline().optionDouble("max-dist")
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
self._citiesMinPopulation = self.configGetInt("poi.minPopulation")
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
self._citiesMinPopulation = self.commandline().optionInt("min-population")
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
self._ampType = self.commandline().optionString("amp-type")
|
|
except BaseException:
|
|
pass
|
|
|
|
try:
|
|
self._ampScript = self.commandline().optionString("amp-script")
|
|
except BaseException:
|
|
try:
|
|
self._ampScript = self.configGetString("scripts.amplitude")
|
|
except BaseException:
|
|
logging.warning("No amplitude script defined")
|
|
|
|
if self._ampScript:
|
|
self._ampScript = system.Environment.Instance().absolutePath(
|
|
self._ampScript
|
|
)
|
|
|
|
try:
|
|
self._alertScript = self.commandline().optionString("alert-script")
|
|
except BaseException:
|
|
try:
|
|
self._alertScript = self.configGetString("scripts.alert")
|
|
except BaseException:
|
|
logging.warning("No alert script defined")
|
|
|
|
if self._alertScript:
|
|
self._alertScript = system.Environment.Instance().absolutePath(
|
|
self._alertScript
|
|
)
|
|
|
|
try:
|
|
self._eventScript = self.commandline().optionString("event-script")
|
|
except BaseException:
|
|
try:
|
|
self._eventScript = self.configGetString("scripts.event")
|
|
logging.info(f"Using event script: {self._eventScript}")
|
|
except BaseException:
|
|
logging.warning("No event script defined")
|
|
|
|
if self._eventScript:
|
|
self._eventScript = system.Environment.Instance().absolutePath(
|
|
self._eventScript
|
|
)
|
|
|
|
logging.info("Creating ringbuffer for 100 objects")
|
|
if not self.query():
|
|
logging.warning("No valid database interface to read from")
|
|
self._cache = datamodel.PublicObjectRingBuffer(self.query(), 100)
|
|
|
|
if self._ampScript and self.connection():
|
|
self.connection().subscribe("AMPLITUDE")
|
|
|
|
if self._newWhenFirstSeen:
|
|
logging.info("A new event is declared when I see it the first time")
|
|
|
|
if not self._agencyIDs:
|
|
logging.info("agencyIDs: []")
|
|
else:
|
|
logging.info(f"agencyIDs: {' '.join(self._agencyIDs)}")
|
|
|
|
return True
|
|
|
|
def printUsage(self):
|
|
|
|
print(
|
|
"""Usage:
|
|
scvoice [options]
|
|
|
|
Alert the user acoustically in real time.
|
|
"""
|
|
)
|
|
|
|
client.Application.printUsage(self)
|
|
|
|
print(
|
|
"""Examples:
|
|
Execute scvoice on command line with debug output
|
|
scvoice --debug
|
|
"""
|
|
)
|
|
|
|
def run(self):
|
|
try:
|
|
try:
|
|
eventID = self.commandline().optionString("eventid")
|
|
event = self._cache.get(datamodel.Event, eventID)
|
|
if event:
|
|
self.notifyEvent(event)
|
|
except BaseException:
|
|
pass
|
|
|
|
return client.Application.run(self)
|
|
except BaseException:
|
|
info = traceback.format_exception(*sys.exc_info())
|
|
for i in info:
|
|
sys.stderr.write(i)
|
|
return False
|
|
|
|
def runAmpScript(self, net, sta, amp):
|
|
if not self._ampScript:
|
|
return
|
|
|
|
if self._ampProc is not None:
|
|
if self._ampProc.poll() is None:
|
|
logging.warning("AmplitudeScript still in progress -> skipping message")
|
|
return
|
|
try:
|
|
self._ampProc = subprocess.Popen([self._ampScript, net, sta, f"{amp:.2f}"])
|
|
logging.info("Started amplitude script with pid %d" % self._ampProc.pid)
|
|
except BaseException:
|
|
logging.error(f"Failed to start amplitude script '{self._ampScript}'")
|
|
|
|
def runAlert(self, lat, lon):
|
|
if not self._alertScript:
|
|
return
|
|
|
|
if self._alertProc is not None:
|
|
if self._alertProc.poll() is None:
|
|
logging.warning("AlertScript still in progress -> skipping message")
|
|
return
|
|
try:
|
|
self._alertProc = subprocess.Popen(
|
|
[self._alertScript, f"{lat:.1f}", f"{lon:.1f}"]
|
|
)
|
|
logging.info("Started alert script with pid %d" % self._alertProc.pid)
|
|
except BaseException:
|
|
logging.error(f"Failed to start alert script '{self._alertScript}'")
|
|
|
|
def done(self):
|
|
self._cache = None
|
|
client.Application.done(self)
|
|
|
|
def handleMessage(self, msg):
|
|
try:
|
|
dm = core.DataMessage.Cast(msg)
|
|
if dm:
|
|
for att in dm:
|
|
org = datamodel.Origin.Cast(att)
|
|
if not org:
|
|
continue
|
|
|
|
try:
|
|
if org.evaluationStatus() == datamodel.PRELIMINARY:
|
|
self.runAlert(
|
|
org.latitude().value(), org.longitude().value()
|
|
)
|
|
except BaseException:
|
|
pass
|
|
|
|
# ao = datamodel.ArtificialOriginMessage.Cast(msg)
|
|
# if ao:
|
|
# org = ao.origin()
|
|
# if org:
|
|
# self.runAlert(org.latitude().value(), org.longitude().value())
|
|
# return
|
|
|
|
client.Application.handleMessage(self, msg)
|
|
except BaseException:
|
|
info = traceback.format_exception(*sys.exc_info())
|
|
for i in info:
|
|
sys.stderr.write(i)
|
|
|
|
def addObject(self, parentID, arg0):
|
|
# pylint: disable=W0622
|
|
try:
|
|
obj = datamodel.Amplitude.Cast(arg0)
|
|
if obj:
|
|
if obj.type() == self._ampType:
|
|
logging.debug(
|
|
f"got new {self._ampType} amplitude '{obj.publicID()}'"
|
|
)
|
|
self.notifyAmplitude(obj)
|
|
|
|
obj = datamodel.Origin.Cast(arg0)
|
|
if obj:
|
|
self._cache.feed(obj)
|
|
logging.debug(f"got new origin '{obj.publicID()}'")
|
|
|
|
try:
|
|
if obj.evaluationStatus() == datamodel.PRELIMINARY:
|
|
self.runAlert(obj.latitude().value(), obj.longitude().value())
|
|
except BaseException:
|
|
pass
|
|
|
|
return
|
|
|
|
obj = datamodel.Magnitude.Cast(arg0)
|
|
if obj:
|
|
self._cache.feed(obj)
|
|
logging.debug(f"got new magnitude '{obj.publicID()}'")
|
|
return
|
|
|
|
obj = datamodel.Event.Cast(arg0)
|
|
if obj:
|
|
org = self._cache.get(datamodel.Origin, obj.preferredOriginID())
|
|
agencyID = org.creationInfo().agencyID()
|
|
logging.debug(f"got new event '{obj.publicID()}'")
|
|
if not self._agencyIDs or agencyID in self._agencyIDs:
|
|
self.notifyEvent(obj, True)
|
|
except BaseException:
|
|
info = traceback.format_exception(*sys.exc_info())
|
|
for i in info:
|
|
sys.stderr.write(i)
|
|
|
|
def updateObject(self, parentID, arg0):
|
|
try:
|
|
obj = datamodel.Event.Cast(arg0)
|
|
if obj:
|
|
org = self._cache.get(datamodel.Origin, obj.preferredOriginID())
|
|
agencyID = org.creationInfo().agencyID()
|
|
logging.debug(f"update event '{obj.publicID()}'")
|
|
if not self._agencyIDs or agencyID in self._agencyIDs:
|
|
self.notifyEvent(obj, False)
|
|
except BaseException:
|
|
info = traceback.format_exception(*sys.exc_info())
|
|
for i in info:
|
|
sys.stderr.write(i)
|
|
|
|
def notifyAmplitude(self, amp):
|
|
self.runAmpScript(
|
|
amp.waveformID().networkCode(),
|
|
amp.waveformID().stationCode(),
|
|
amp.amplitude().value(),
|
|
)
|
|
|
|
def notifyEvent(self, evt, newEvent=True):
|
|
try:
|
|
org = self._cache.get(datamodel.Origin, evt.preferredOriginID())
|
|
if not org:
|
|
logging.warning(
|
|
"unable to get origin %s, ignoring event "
|
|
"message" % evt.preferredOriginID()
|
|
)
|
|
return
|
|
|
|
preliminary = False
|
|
try:
|
|
if org.evaluationStatus() == datamodel.PRELIMINARY:
|
|
preliminary = True
|
|
except BaseException:
|
|
pass
|
|
|
|
if not preliminary:
|
|
nmag = self._cache.get(datamodel.Magnitude, evt.preferredMagnitudeID())
|
|
if nmag:
|
|
mag = nmag.magnitude().value()
|
|
mag = f"magnitude {mag:.1f}"
|
|
else:
|
|
if len(evt.preferredMagnitudeID()) > 0:
|
|
logging.warning(
|
|
"unable to get magnitude %s, ignoring event "
|
|
"message" % evt.preferredMagnitudeID()
|
|
)
|
|
else:
|
|
logging.warning(
|
|
"no preferred magnitude yet, ignoring event message"
|
|
)
|
|
return
|
|
|
|
# keep track of old events
|
|
if self._newWhenFirstSeen:
|
|
if evt.publicID() in self._prevMessage:
|
|
newEvent = False
|
|
else:
|
|
newEvent = True
|
|
|
|
dsc = seismology.Regions.getRegionName(
|
|
org.latitude().value(), org.longitude().value()
|
|
)
|
|
|
|
if self._eventDescriptionPattern:
|
|
try:
|
|
city, dist, _ = self.nearestCity(
|
|
org.latitude().value(),
|
|
org.longitude().value(),
|
|
self._citiesMaxDist,
|
|
self._citiesMinPopulation,
|
|
)
|
|
if city:
|
|
dsc = self._eventDescriptionPattern
|
|
region = seismology.Regions.getRegionName(
|
|
org.latitude().value(), org.longitude().value()
|
|
)
|
|
distStr = str(int(math.deg2km(dist)))
|
|
dsc = (
|
|
dsc.replace("@region@", region)
|
|
.replace("@dist@", distStr)
|
|
.replace("@poi@", city.name())
|
|
)
|
|
except BaseException:
|
|
pass
|
|
|
|
logging.debug(f"desc: {dsc}")
|
|
|
|
dep = org.depth().value()
|
|
now = core.Time.GMT()
|
|
otm = org.time().value()
|
|
|
|
dt = (now - otm).seconds()
|
|
|
|
# if dt > dtmax:
|
|
# return
|
|
|
|
if dt > 3600:
|
|
dt = "%d hours %d minutes ago" % (int(dt / 3600), int((dt % 3600) / 60))
|
|
elif dt > 120:
|
|
dt = "%d minutes ago" % int(dt / 60)
|
|
else:
|
|
dt = "%d seconds ago" % int(dt)
|
|
|
|
if preliminary:
|
|
message = "earthquake, preliminary, %%s, %s" % dsc
|
|
else:
|
|
message = "earthquake, %%s, %s, %s, depth %d kilometers" % (
|
|
dsc,
|
|
mag,
|
|
int(dep + 0.5),
|
|
)
|
|
# at this point the message lacks the "ago" part
|
|
|
|
if (
|
|
evt.publicID() in self._prevMessage
|
|
and self._prevMessage[evt.publicID()] == message
|
|
):
|
|
logging.info(f"Suppressing repeated message '{message}'")
|
|
return
|
|
|
|
self._prevMessage[evt.publicID()] = message
|
|
message = message % dt # fill the "ago" part
|
|
logging.info(message)
|
|
|
|
if not self._eventScript:
|
|
return
|
|
|
|
if self._eventProc is not None:
|
|
if self._eventProc.poll() is None:
|
|
logging.warning("EventScript still in progress -> skipping message")
|
|
return
|
|
|
|
try:
|
|
param2 = 0
|
|
param3 = 0
|
|
param4 = ""
|
|
if newEvent:
|
|
param2 = 1
|
|
|
|
org = self._cache.get(datamodel.Origin, evt.preferredOriginID())
|
|
if org:
|
|
try:
|
|
param3 = org.quality().associatedPhaseCount()
|
|
except BaseException:
|
|
pass
|
|
|
|
nmag = self._cache.get(datamodel.Magnitude, evt.preferredMagnitudeID())
|
|
if nmag:
|
|
param4 = f"{nmag.magnitude().value():.1f}"
|
|
|
|
self._eventProc = subprocess.Popen(
|
|
[
|
|
self._eventScript,
|
|
message,
|
|
"%d" % param2,
|
|
evt.publicID(),
|
|
"%d" % param3,
|
|
param4,
|
|
]
|
|
)
|
|
logging.info("Started event script with pid %d" % self._eventProc.pid)
|
|
except BaseException:
|
|
logging.error(
|
|
"Failed to start event script '%s %s %d %d %s'"
|
|
% (self._eventScript, message, param2, param3, param4)
|
|
)
|
|
except BaseException:
|
|
info = traceback.format_exception(*sys.exc_info())
|
|
for i in info:
|
|
sys.stderr.write(i)
|
|
|
|
|
|
app = VoiceAlert(len(sys.argv), sys.argv)
|
|
sys.exit(app())
|