Beta Release
This commit is contained in:
@@ -1,7 +1,26 @@
|
||||
scale_interfaces:
|
||||
- usb_pce
|
||||
read_scale_interval_sec: 5
|
||||
beielipi_mobile_number: "077 461 19 91"
|
||||
scales:
|
||||
- alias: "Waage Volk 1"
|
||||
scale_uuid: "46335715-8454-483b-a17b-571619c5015d"
|
||||
interface_type: usb_pce
|
||||
interface_name: ttyUSB0
|
||||
sms_alert_phonenumbers:
|
||||
- "+41765006123"
|
||||
- alias: "Waage Volk 2"
|
||||
scale_uuid: "46335715-8454-483b-a17b-571619c5015e"
|
||||
interface_type: dummy
|
||||
sms_alert_phonenumbers:
|
||||
- "+41765006124"
|
||||
read_scale_interval_sec: 300
|
||||
number_of_samples: 5
|
||||
sms_alert_phonenumbers:
|
||||
- "0765006123"
|
||||
swarm_alarm_threshold_gram: 50
|
||||
swarm_alarm_threshold_gram: 500
|
||||
mailserver: mail.nbit.ch
|
||||
mailserver_port: 587
|
||||
mailfrom: info@nbit.ch
|
||||
mailto: joerg.lehmann@nbit.ch
|
||||
mailuser: nbitinf@nbit.ch
|
||||
mailpwd: ukihefak27
|
||||
balance_number: "444"
|
||||
balance_command: "STATUS"
|
||||
forward_sms_from_this_number: "444"
|
||||
master_sms_number: "+41765006123"
|
||||
|
||||
+111
-20
@@ -11,15 +11,21 @@
|
||||
|
||||
from __future__ import print_function
|
||||
import gammu.smsd
|
||||
import os
|
||||
import sys
|
||||
import serial
|
||||
import time
|
||||
import yaml
|
||||
import random
|
||||
|
||||
# Root Path
|
||||
APP_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
# Read Configuration from YAML-File
|
||||
with open("beielimon-config.yaml", 'r') as stream:
|
||||
with open("%s/bin/beielimon-config.yaml" % (APP_ROOT), 'r') as stream:
|
||||
try:
|
||||
config_data = yaml.load(stream)
|
||||
#print(config_data)
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
|
||||
@@ -30,22 +36,43 @@ INVALID_VALUE = -999
|
||||
smsd = gammu.smsd.SMSD('/etc/gammu-smsdrc')
|
||||
|
||||
class Scale(object):
|
||||
def __init__(self):
|
||||
def __init__(self, scale_config):
|
||||
self.last_values = []
|
||||
self.scale_config = scale_config
|
||||
|
||||
def __del__(self):
|
||||
pass
|
||||
|
||||
def LogValue(self,weigh_in_gram,swarm_alarm):
|
||||
cur_time = time.localtime()
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M", cur_time)
|
||||
year = time.strftime("%Y", cur_time)
|
||||
month = time.strftime("%m", cur_time)
|
||||
day = time.strftime("%d", cur_time)
|
||||
if swarm_alarm:
|
||||
prefix = 'swarmalarm'
|
||||
else:
|
||||
prefix = 'weight'
|
||||
datafilename = "%s/data/%s-%s-%s%s%s.log" % (APP_ROOT,prefix,self.scale_config['scale_uuid'],year,month,day)
|
||||
#print('Log to File %s' % (datafilename))
|
||||
with open(datafilename, 'a') as file:
|
||||
file.write('%s,%d\n' % (timestamp,weigh_in_gram))
|
||||
|
||||
def AppendReading(self,weigh_in_gram):
|
||||
self.last_values.append(weigh_in_gram)
|
||||
if len(self.last_values) > config_data['number_of_samples']:
|
||||
self.last_values = self.last_values[1:]
|
||||
print('DEBUG WEIGHT: %d' % (weigh_in_gram))
|
||||
print(self.last_values)
|
||||
#print('DEBUG WEIGHT: %d' % (weigh_in_gram))
|
||||
#print(self.last_values)
|
||||
# Wir loggen den Wert noch
|
||||
self.LogValue(weigh_in_gram,False)
|
||||
|
||||
def Read(self):
|
||||
pass
|
||||
|
||||
def CalibrateToZero(self):
|
||||
pass
|
||||
|
||||
def SwarmAlarm(self):
|
||||
return (self.GetWeighLoss() > config_data['swarm_alarm_threshold_gram'])
|
||||
|
||||
@@ -58,13 +85,16 @@ class Scale(object):
|
||||
def GetWeighLoss(self):
|
||||
last_value = self.GetLastValue()
|
||||
max_value = max(self.last_values or [0])
|
||||
print('BBB: ',max_value,last_value)
|
||||
#print('BBB: ',max_value,last_value)
|
||||
return (max_value - last_value)
|
||||
|
||||
def GetScaleConfig(self):
|
||||
return self.scale_config
|
||||
|
||||
|
||||
class ScaleUSB_PCE(Scale):
|
||||
def __init__(self,serial_int):
|
||||
Scale.__init__(self)
|
||||
def __init__(self,serial_int, scale_config):
|
||||
Scale.__init__(self, scale_config)
|
||||
self.ser = serial_int
|
||||
|
||||
def Read(self):
|
||||
@@ -84,12 +114,57 @@ class ScaleUSB_PCE(Scale):
|
||||
if res != INVALID_VALUE:
|
||||
self.AppendReading(res)
|
||||
|
||||
class ScaleBT_KDPSB(Scale):
|
||||
def __init__(self,serial_int, scale_config):
|
||||
Scale.__init__(self, scale_config)
|
||||
self.ser = serial_int
|
||||
|
||||
def Read(self):
|
||||
res = INVALID_VALUE
|
||||
try:
|
||||
self.ser.write('\x02G\x03')
|
||||
except:
|
||||
print('DEBUG FEHLER BEIM SCHREIBEN')
|
||||
time.sleep(1)
|
||||
try:
|
||||
weight_string = self.ser.read(12)
|
||||
except:
|
||||
print('DEBUG FEHLER BEIM LESEN')
|
||||
weight_string=''
|
||||
#print('DEBUG READ STRING HEX: %s' % (':'.join(x.encode('hex') for x in weight_string)))
|
||||
#print('DEBUG READ STRING: %s' % (weight_string))
|
||||
if len(weight_string) == 12:
|
||||
#print('DEBUG READ STRING BBB: %s' % (weight_string[1:8]))
|
||||
res = int(float(weight_string[1:8])*1000)
|
||||
#print('DEBUG GEWICHT IN GRAM: %s' % (weight_string))
|
||||
if res != INVALID_VALUE:
|
||||
self.AppendReading(res)
|
||||
|
||||
def CalibrateToZero(self):
|
||||
try:
|
||||
self.ser.write('\x02T\x03')
|
||||
except:
|
||||
print('DEBUG FEHLER BEIM KALIBRIEREN')
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
class ScaleDummy(Scale):
|
||||
def __init__(self,serial_int):
|
||||
pass
|
||||
def __init__(self,scale_config):
|
||||
Scale.__init__(self, scale_config)
|
||||
|
||||
def Read(self):
|
||||
pass
|
||||
# Gewichts- Zu/Abnahme ist Random, manchmal gibt es einen
|
||||
# Zufaelligen Schwarmalarm
|
||||
delta = random.randint(-2, 4)
|
||||
if (random.randint(0,1000) == 500):
|
||||
delta = random.randint(-1000, - 501)
|
||||
|
||||
last_value = self.GetLastValue()
|
||||
# Wir duerfen nicht negativ werden...
|
||||
if (last_value + delta < 0):
|
||||
delta = random.randint(0,4)
|
||||
|
||||
self.AppendReading(last_value + delta)
|
||||
|
||||
|
||||
def send_sms(phonenumbers , text):
|
||||
@@ -100,18 +175,33 @@ def send_sms(phonenumbers , text):
|
||||
'Number': phonenumber
|
||||
}
|
||||
|
||||
smsd.InjectSMS([message])
|
||||
#GELD SPAREN smsd.InjectSMS([message])
|
||||
#print("Send SMS to %s, Text: %s" % (phonenumber, text))
|
||||
|
||||
def main():
|
||||
scales = []
|
||||
for scale_interface in config_data['scale_interfaces']:
|
||||
if scale_interface == 'usb_pce':
|
||||
ser = serial.Serial(port='/dev/ttyUSB0',
|
||||
for scale_config in config_data['scales']:
|
||||
if scale_config['interface_type'] == 'bt_kdpsb':
|
||||
#print('DEBUG: sudo /usr/bin/rfcomm bind ' + scale_config['interface_name'] + ' ' + scale_config['address'] + ' ' + scale_config['interface_channel'])
|
||||
os.system('sudo /usr/bin/rfcomm bind ' + scale_config['interface_name'] + ' ' + scale_config['address'] + ' ' + scale_config['interface_channel'])
|
||||
ser = serial.Serial(port='/dev/' + scale_config['interface_name'],
|
||||
baudrate=9600,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_EVEN,
|
||||
timeout=20000)
|
||||
scale=ScaleUSB_PCE(ser)
|
||||
timeout=20)
|
||||
scale=ScaleBT_KDPSB(ser, scale_config)
|
||||
#scale.CalibrateToZero()
|
||||
scales.append(scale)
|
||||
elif scale_config['interface_type'] == 'usb_pce':
|
||||
ser = serial.Serial(port='/dev/' + scale_config['interface_name'],
|
||||
baudrate=9600,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_EVEN,
|
||||
timeout=20)
|
||||
scale=ScaleUSB_PCE(ser, scale_config)
|
||||
scales.append(scale)
|
||||
elif scale_config['interface_type'] == 'dummy':
|
||||
scale=ScaleDummy(scale_config)
|
||||
scales.append(scale)
|
||||
|
||||
# Main Loop
|
||||
@@ -119,12 +209,13 @@ def main():
|
||||
for scale in scales:
|
||||
scale.Read()
|
||||
if scale.SwarmAlarm():
|
||||
date_time = time.strftime("%d.%m.%Y %H:%M:%S")
|
||||
date_time = time.strftime("%d.%m.%Y %H:%M")
|
||||
last_value = scale.GetLastValue()
|
||||
weigh_loss = scale.GetWeighLoss()
|
||||
sms_message = '*** Schwarmalarm ***\nDatum/Zeit: \nLetztes Gewicht [g]: %d\nGewichtsverlust [g]: %d' % (date_time,last_value,weigh_loss)
|
||||
print(config_data['sms_alert_phonenumbers'],sms_message)
|
||||
send_sms(config_data['sms_alert_phonenumbers'],sms_message)
|
||||
# Wir loggen den Wert noch
|
||||
scale.LogValue(last_value,True)
|
||||
sms_message = '*** Schwarmalarm ***\nDatum/Zeit: %s\nWaage: %s\nLetztes Gewicht [g]: %d\nGewichtsverlust [g]: %d' % (date_time,scale.GetScaleConfig()['alias'],last_value,weigh_loss)
|
||||
send_sms(scale.GetScaleConfig()['sms_alert_phonenumbers'],sms_message)
|
||||
scale.ResetValues()
|
||||
|
||||
time.sleep(config_data['read_scale_interval_sec'])
|
||||
|
||||
Executable
+284
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# vim: expandtab sw=4 ts=4 sts=4:
|
||||
#
|
||||
# Beehive-Monitoring, process SMS Requests
|
||||
#
|
||||
# Author: Joerg Lehmann, nbit Informatik GmbH
|
||||
#
|
||||
"""Beehive Monitoring - SMS Processing"""
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import yaml
|
||||
import smtplib
|
||||
import re
|
||||
import glob
|
||||
import shutil
|
||||
from os.path import basename
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import COMMASPACE, formatdate
|
||||
|
||||
# Root Path
|
||||
APP_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
# Read Configuration from YAML-File
|
||||
with open("%s/bin/beielimon-config.yaml" % APP_ROOT, 'r') as stream:
|
||||
try:
|
||||
config_data = yaml.load(stream)
|
||||
#print(config_data)
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
|
||||
def send_sms(phonenumbers , text):
|
||||
print("BBB: %s" % (text))
|
||||
for phonenumber in phonenumbers:
|
||||
message = {
|
||||
'Text': text,
|
||||
'SMSC': {'Location': 1},
|
||||
'Number': phonenumber
|
||||
}
|
||||
|
||||
#os.system('echo "%s" | /usr/bin/gammu-smsd-inject TEXT %s' % (text,phonenumber))
|
||||
print("YYY Send SMS to %s, Text: %s" % (phonenumber, text))
|
||||
|
||||
def send_mail(send_from, send_to, subject, text, files=None):
|
||||
os.system('/usr/bin/sudo %s/root-bin/connect_to_internet' % (APP_ROOT))
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = send_from
|
||||
msg['To'] = send_to
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
msg['Subject'] = subject
|
||||
|
||||
msg.attach(MIMEText(text))
|
||||
|
||||
print("XXX: %s" % (files))
|
||||
for f in files or []:
|
||||
print("AAA: %s" % (f))
|
||||
with open(f, "rb") as fil:
|
||||
part = MIMEApplication(
|
||||
fil.read(),
|
||||
Name=basename(f)
|
||||
)
|
||||
# After the file is closed
|
||||
part['Content-Disposition'] = 'attachment; filename="%s"' % basename(f)
|
||||
msg.attach(part)
|
||||
|
||||
smtp = smtplib.SMTP(config_data['mailserver'],config_data['mailserver_port'],timeout=30)
|
||||
#smtp.set_debuglevel(1)
|
||||
smtp.ehlo()
|
||||
smtp.starttls()
|
||||
smtp.login(config_data['mailuser'], config_data['mailpwd'])
|
||||
smtp.sendmail(send_from, send_to, msg.as_string())
|
||||
smtp.close()
|
||||
os.system('/usr/bin/sudo %s/root-bin/disconnect_from_internet' % (APP_ROOT))
|
||||
|
||||
def send_help(phonenumber,command):
|
||||
# SMS Maximale Groesse: 140 Zeichen
|
||||
if command == '':
|
||||
sms_message = """Moegliche Befehle:
|
||||
|
||||
help, info, balance, reboot, shutdown, hotspot
|
||||
|
||||
Naehere Hilfe: help <befehl>, z.B. help info"""
|
||||
|
||||
elif command == 'info':
|
||||
sms_message = """info - letzte Messwerte (SMS)
|
||||
info 2017 - Messwerte 2017 (EMail)
|
||||
info 201711 - Messwerte Nov. 2017
|
||||
info 20171102 - Messwerte 2. Nov. 2017"""
|
||||
|
||||
elif command == 'help':
|
||||
sms_message = "Zeigt die moeglichen Befehle an"
|
||||
|
||||
elif command == 'balance':
|
||||
sms_message = "Anforderung Info zum Prepaid-Guthaben"
|
||||
|
||||
elif command == 'reboot':
|
||||
sms_message = "Neustart des Raspberry Pi's"
|
||||
|
||||
elif command == 'shutdown':
|
||||
sms_message = """Herunterfahren des Raspberry Pi's
|
||||
|
||||
ACHTUNG: ZUM STARTEN MUSS STROM AUS- UND WIEDER EINGESTECKT WERDEN!
|
||||
"""
|
||||
|
||||
elif command == 'hotspot':
|
||||
sms_message = """hotspot on - Hotspot Funktion einschalten
|
||||
hotspot off - Hotspot ausschalten
|
||||
|
||||
Hotspot erlaubt ein Auslesen per Smartphone/Table (WLAN)"""
|
||||
|
||||
send_sms([phonenumber],sms_message)
|
||||
|
||||
|
||||
def GetLastValues(scale_uuid):
|
||||
files = glob.glob("%s/data/weight-%s-????????.log" % (APP_ROOT,scale_uuid))
|
||||
if len(files) > 0:
|
||||
with open(sorted(files)[-1], "r") as f:
|
||||
for line in f: pass
|
||||
result = line
|
||||
|
||||
|
||||
# Beispiel: 2017-11-07 08:23,0
|
||||
m = re.match("(\d{4})-(\d{2})-(\d{2}) (\d\d:\d\d),(\d+)", result)
|
||||
if m:
|
||||
return "%sg (%s.%s.%s %s)" % (m.group(5),m.group(3),m.group(2),m.group(1),m.group(4))
|
||||
else:
|
||||
return "Fehler beim Parsen: %s" % (result)
|
||||
|
||||
else:
|
||||
return "keine Messwerte"
|
||||
|
||||
def CreateAttachements(infotime):
|
||||
mypid = os.getpid()
|
||||
mycsvdir = "%s/tmp/%s/csv" % (APP_ROOT,mypid)
|
||||
if not os.path.exists(mycsvdir):
|
||||
os.makedirs(mycsvdir)
|
||||
myzipdir = "%s/tmp/%s/zip" % (APP_ROOT,mypid)
|
||||
if not os.path.exists(myzipdir):
|
||||
os.makedirs(myzipdir)
|
||||
res = []
|
||||
my_pattern = '%s%s' % (infotime,'?' * (8 - len(infotime)))
|
||||
print('AAA: %s' % (my_pattern))
|
||||
files = glob.glob("%s/data/weight-*-%s.log" % (APP_ROOT,my_pattern))
|
||||
for f in files:
|
||||
print("%s" % (f))
|
||||
for s in config_data['scales']:
|
||||
filename = "%s/%s-%s.csv" % (mycsvdir,s['alias'].replace(' ','_'),infotime)
|
||||
with_data = False
|
||||
with open(filename, 'a') as file:
|
||||
for ifile in sorted(files):
|
||||
if (s['scale_uuid'] in ifile) and (infotime in ifile):
|
||||
with_data = True
|
||||
with open(ifile, 'r') as ifile:
|
||||
for line in ifile:
|
||||
m = re.match("(\d{4})-(\d{2})-(\d{2}) (\d\d:\d\d,(\d+)", line)
|
||||
if m:
|
||||
file.write("%s.%s.%s %s,%s\n" % (m.group(3),m.group(2),m.group(1),m.group(4),m.group(5)))
|
||||
else:
|
||||
file.write("Fehler beim Parsen: %s" % (line))
|
||||
if with_data:
|
||||
res.append(filename)
|
||||
|
||||
zipfile = "%s/%s" % (myzipdir,infotime)
|
||||
shutil.make_archive(zipfile, 'zip', mycsvdir)
|
||||
#print("ATTA: %s" % (res))
|
||||
return [ "%s.zip" % (zipfile) ]
|
||||
|
||||
def send_report(infotime):
|
||||
# Send EMail Report
|
||||
my_text = "Letzte Messwerte:\n"
|
||||
for s in config_data['scales']:
|
||||
my_text += "%s: %s\n" % (s['alias'],GetLastValues(s['scale_uuid']))
|
||||
attachements = CreateAttachements(infotime)
|
||||
|
||||
#send_mail(config_data['mailfrom'],config_data['mailto'],'Messwerte mini-beieli',my_text,attachements)
|
||||
|
||||
def send_info_sms(phonenumber):
|
||||
my_text = "Letzte Messwerte:\n"
|
||||
for s in config_data['scales']:
|
||||
my_text += "%s: %s\n" % (s['alias'],GetLastValues(s['scale_uuid']))
|
||||
|
||||
send_sms([phonenumber],my_text)
|
||||
|
||||
def send_info(phonenumber, message_uc):
|
||||
m = re.match(".*(INFO)\s+(\d{8})", message_uc)
|
||||
if m:
|
||||
send_report(m.group(2))
|
||||
else:
|
||||
m = re.match(".*(INFO)\s+(\d{6})", message_uc)
|
||||
if m:
|
||||
send_report(m.group(2))
|
||||
else:
|
||||
m = re.match(".*(INFO)\s+(\d{4})", message_uc)
|
||||
if m:
|
||||
send_report(m.group(2))
|
||||
else:
|
||||
send_info_sms(phonenumber)
|
||||
|
||||
def balance():
|
||||
send_sms([config_data['balance_number']],config_data['balance_command'])
|
||||
|
||||
def reboot():
|
||||
os.system('/usr/bin/sudo /sbin/init 6')
|
||||
|
||||
def shutdown():
|
||||
os.system('/usr/bin/sudo /sbin/init 0')
|
||||
|
||||
def hotspot_on():
|
||||
os.system('/usr/bin/sudo %s/root-bin/hotspot on' % (APP_ROOT))
|
||||
|
||||
def hotspot_off():
|
||||
os.system('/usr/bin/sudo %s/root-bin/hotspot off' % (APP_ROOT))
|
||||
|
||||
def command_not_understood(phonenumber, message):
|
||||
send_sms([phonenumber],'Befehl nicht verstanden: %s\n\nMoegliche Befehle: help, info, balance, reboot, shutdown, hotspot' % (message[:50]))
|
||||
|
||||
def main():
|
||||
print(config_data)
|
||||
print('Number of arguments:', len(sys.argv), 'arguments.')
|
||||
print('Argument List:', str(sys.argv))
|
||||
if len(sys.argv) != 3:
|
||||
print("Da kann etwas nicht stimmen, ungueltige Anzahl Argumente")
|
||||
sys.exit(1)
|
||||
|
||||
phonenumber = sys.argv[1]
|
||||
message = sys.argv[2]
|
||||
|
||||
# Falls es von forward_sms_from_this_number kommt, machen wir ein Foward an
|
||||
# die Master Nummer
|
||||
if phonenumber == config_data['forward_sms_from_this_number']:
|
||||
print("AAA: %s" % (message))
|
||||
#send_mail(config_data['mailfrom'],config_data['mailto'],'Beielimon Subject',message,[])
|
||||
send_sms([config_data['master_sms_number']],message)
|
||||
|
||||
else:
|
||||
# message in Grossbuchstaben (damit Gross-/Kleinschreibung keine Rolle spielt)
|
||||
message_uc = message.upper()
|
||||
|
||||
# Bestimmung, ob es eine gueltige Telefonnummer ist
|
||||
valid_number = False
|
||||
for s in config_data['scales']:
|
||||
if phonenumber in s['sms_alert_phonenumbers']:
|
||||
valid_number = True
|
||||
|
||||
if not valid_number:
|
||||
print("Da versucht ein unberechtigter, etwas abzufragen... (Nummer: %s)" % (phonenumber))
|
||||
sys.exit(2)
|
||||
|
||||
if 'HELP INFO' in message_uc:
|
||||
send_help(phonenumber,'info')
|
||||
elif 'HELP HELP' in message_uc:
|
||||
send_help(phonenumber,'help')
|
||||
elif 'HELP BALANCE' in message_uc:
|
||||
send_help(phonenumber,'balance')
|
||||
elif 'HELP REBOOT' in message_uc:
|
||||
send_help(phonenumber,'reboot')
|
||||
elif 'HELP SHUTDOWN' in message_uc:
|
||||
send_help(phonenumber,'shutdown')
|
||||
elif 'HELP HOTSPOT' in message_uc:
|
||||
send_help(phonenumber,'hotspot')
|
||||
elif 'HELP' in message_uc:
|
||||
send_help(phonenumber,'')
|
||||
elif 'INFO' in message_uc:
|
||||
send_info(phonenumber, message_uc)
|
||||
elif 'BALANCE' in message_uc:
|
||||
balance()
|
||||
elif 'REBOOT' in message_uc:
|
||||
reboot()
|
||||
elif 'SHUTDOWN' in message_uc:
|
||||
shutdown()
|
||||
elif 'HOTSPOT OFF' in message_uc:
|
||||
hotspot_off()
|
||||
elif 'HOTSPOT ON' in message_uc:
|
||||
hotspot_on()
|
||||
else:
|
||||
command_not_understood(phonenumber, message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user