123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- #!/usr/bin/python
- __author__ = "Kruz, Wazakindjes"
- __copyright__ = "Jem0eder Inc."
- __version__ = "2.0.0"
- import argparse
- import ConfigParser
- import datetime
- import json
- import MySQLdb
- from onesignal import OneSignal
- import os
- import requests
- import sys
- import time
- from var_dump import var_dump
- # Some "constants" lol
- RET_GUCCI = 0
- RET_ERR_CONF = 1
- RET_ERR_SENSOR = 2
- RET_ERR_WEATHERMON = 3
- # Globals y0
- muhconf = {}
- skripdir = os.path.dirname(os.path.realpath(__file__))
- def printem(obj):
- # Because apparently we need to flush after every print to ensure it happens in real-time -.-
- # Also this waii we can check if the object is properly printable by default, some objects may not be so let's use var_dump() for that shit instead ;]
- if isinstance(obj, (float, int, str, list, dict, tuple)):
- print obj
- else:
- var_dump(obj)
- sys.stdout.flush()
- def try_db_cleanup(db, cursor):
- # Some objects may not exist and perhaps db does but not cursor, hence 2 different try blocks ;]
- # It shouldn't really be a problem if we quit before closing the connection, but it *is* good practice to close em cleanly
- try:
- cursor.close()
- except:
- pass
- try:
- db.close()
- except:
- pass
- def show_version():
- printem("temps.py v{0}, written by: {1}".format(__version__, __author__))
- def readem_conf():
- global muhconf
- err = False
- muhconf = {
- "main": {},
- "mysql": {},
- "weathermon": {},
- }
- try:
- cfg = ConfigParser.SafeConfigParser({
- # Some variables are optional ;]
- # [main] section
- "debug": "false",
- "dryrun": "false",
- "sample_maxattempts": "5",
- # [mysql] section
- "port": "3306",
- "tls": "false",
- # [weathermon] section
- "enable": "false",
- "tls_ca_file": "/etc/ssl/certs/ca-certificates.crt",
- "onesignal_segment": None,
- # The other variables (interval, url, key) are not optional if enable == true, so they shouldn't be specified here either ;]
- })
- cfg.read("{0}/muhconf.ini".format(skripdir))
- ### [main] section BEGIN
- # Booleans first
- for main_boolopt in ["debug", "dryrun"]:
- muhconf["main"][main_boolopt] = cfg.getboolean("main", main_boolopt)
- # Integers next
- for main_intopt in ["sample_maxattempts"]:
- optval = cfg.getint("main", main_intopt)
- # Should prolly restrict it to 30 attempts, anything more seems way too excessive [==[[==[[=
- if main_intopt == "sample_maxattempts":
- if optval <= 0 or optval > 30:
- printem("Invalid value '{0}' for option '{1}': must be in the range of 1-30".format(optval, main_intopt))
- err = True
- continue
- muhconf["main"][main_intopt] = optval
- # Multiple string values last
- for main_stropt_multi in ["sensor"]:
- optval_multi = cfg.get("main", main_stropt_multi).split("\n")
- for optval in optval_multi:
- if len(optval) == 0:
- printem("Invalid value for option '{0}': may not be left empty".format(main_stropt_multi))
- err = True
- continue
- if main_stropt_multi == "sensor":
- if "/" in optval:
- printem("Invalid value '{0}' for option '{1}': may not contain a slash".format(optval, main_stropt_multi))
- err = True
- continue
- if not os.access("/sys/bus/w1/devices/{0}/w1_slave".format(optval), os.R_OK):
- printem("Invalid value '{0}' for option '{1}': file '/sys/bus/w1/devices/{0}/w1_slave' doesn't exist or is not readable".format(optval, main_stropt_multi))
- err = True
- continue
- muhconf["main"][main_stropt_multi] = optval_multi
- ### [main] section END
- ### [mysql] section BEGIN
- # Booleans first
- for mysql_boolopt in ["tls"]:
- muhconf["mysql"][mysql_boolopt] = cfg.getboolean("mysql", mysql_boolopt)
- # Integers next
- for mysql_intopt in ["port"]:
- optval = cfg.getint("mysql", mysql_intopt)
- if mysql_intopt == "port":
- if optval <= 0 or optval > 65535:
- printem("Invalid value '{0}' for option '{1}': must be in the range of 1-65535".format(optval, mysql_intopt))
- err = True
- continue
- muhconf["mysql"][mysql_intopt] = optval
- # String values last
- for mysql_stropt in ["host", "user", "pass", "db", "table", "column"]:
- optval = cfg.get("mysql", mysql_stropt)
- if len(optval) == 0:
- printem("Invalid value for option '{0}': may not be left empty".format(mysql_stropt))
- err = True
- continue
- muhconf["mysql"][mysql_stropt] = optval
- ### [mysql] section END
- ### [weathermon] section BEGIN
- # Booleans first
- for weathermon_boolopt in ["enable"]:
- muhconf["weathermon"][weathermon_boolopt] = cfg.getboolean("weathermon", weathermon_boolopt)
- if muhconf["weathermon"]["enable"]:
- # Integers next, but only if shit's enabled lol (obviously)
- for weathermon_intopt in ["interval", "city_id"]:
- optval = cfg.getint("weathermon", weathermon_intopt)
- # Free weather APIs generally don't update faster than once every 10 minutes, but you should checkem at least once per hour
- if weathermon_intopt == "interval":
- if optval < 10 or optval > 60:
- printem("Invalid value '{0}' for option '{1}': must be in the range of 10-60".format(optval, weathermon_intopt))
- err = True
- continue
- elif weathermon_intopt == "city_id":
- if optval <= 0:
- printem("Invalid value '{0}' for option '{1}': must be greater than 0".format(optval, weathermon_intopt))
- err = True
- continue
- muhconf["weathermon"][weathermon_intopt] = optval
- # String values last
- for weathermon_stropt in ["weather_url", "weather_key", "tls_ca_file", "onesignal_restkey", "onesignal_appid", "onesignal_channel", "onesignal_segment"]:
- optval = cfg.get("weathermon", weathermon_stropt)
- # Cuz certain optional vars default to None lol (missing keys throw an exception ;])
- if optval is None:
- muhconf["weathermon"][weathermon_stropt] = optval
- continue
- if len(optval) == 0:
- printem("Invalid value for option '{0}': may not be left empty".format(weathermon_stropt))
- err = True
- continue
- elif weathermon_stropt in ["weather_url"]:
- if not optval.startswith("https://"):
- printem("Invalid value '{0}' for option '{1}': must start with https://".format(optval, weathermon_stropt))
- err = True
- continue
- elif weathermon_stropt == "tls_ca_file":
- if not optval.startswith("/"):
- printem("Invalid value '{0}' for option '{1}': must start with a slash (absolute path y0)".format(optval, weathermon_stropt))
- err = True
- continue
- if not os.access(optval, os.R_OK):
- printem("Invalid value '{0}' for option '{1}': file doesn't exist or is not readable".format(optval, weathermon_stropt))
- err = True
- continue
- muhconf["weathermon"][weathermon_stropt] = optval
- ### [weathermon] section END
- except KeyboardInterrupt:
- printem("\nCTRL + C")
- sys.exit(RET_GUCCI)
- except:
- exc_info = sys.exc_info()
- printem("\nRIP config ({0}): {1}".format(exc_info[0], exc_info[1]))
- sys.exit(RET_ERR_CONF)
- if err:
- printem("Invalid config, not proceeding")
- sys.exit(RET_ERR_CONF)
- def readem_temp():
- avgtemperatures = []
- db = None
- cursor = None
- try:
- if muhconf["main"]["debug"]:
- printem("Connecting to MySQL lol")
- if muhconf["mysql"]["tls"]:
- db = MySQLdb.connect(host=muhconf["mysql"]["host"],
- port=muhconf["mysql"]["port"],
- user=muhconf["mysql"]["user"],
- passwd=muhconf["mysql"]["pass"],
- db=muhconf["mysql"]["db"],
- ssl={"cipher": "AES256-SHA"})
- else:
- db = MySQLdb.connect(host=muhconf["mysql"]["host"],
- port=muhconf["mysql"]["port"],
- user=muhconf["mysql"]["user"],
- passwd=muhconf["mysql"]["pass"],
- db=muhconf["mysql"]["db"])
- cursor = db.cursor()
- if muhconf["main"]["debug"]:
- printem("Connected y0")
- for sensor in muhconf["main"]["sensor"]:
- if muhconf["main"]["debug"]:
- printem("\tSensor: {0}".format(sensor))
- temperatures = []
- for i in range(1, 6): # Goes _up to_ 6 so the last iteration is i == 5 ;]
- text = '';
- attempts = 0
- while text.split("\n")[0].find("YES") == -1:
- if attempts >= muhconf["main"]["sample_maxattempts"]:
- plural = "s" if attempts > 1 else ""
- if muhconf["main"]["debug"]:
- printem("RIP readem_temp(): unable to read temperature after {0} attempt{1} (sample #{2})".format(attempts, plural, i))
- else:
- # Non-debug mode doesn't print the sensor name at an earlier point, so let's make it clear here ;;]];];
- printem("RIP readem_temp(): unable to read temperature for sensor {0} after {1} attempt{2} (sample #{3})".format(sensor, attempts, plural, i))
- sys.exit(RET_ERR_SENSOR)
- if attempts > 0:
- # The sensor needs a 1 second recalibration time
- time.sleep(1)
- # Contents of the file should be something leik dis:
- # 79 01 80 80 7f ff 7f 80 02 : crc=02 YES
- # 79 01 80 80 7f ff 7f 80 02 t=23562
- tfile = open("/sys/bus/w1/devices/{0}/w1_slave".format(sensor), "r")
- text = tfile.read()
- tfile.close()
- attempts += 1
- secondline = text.split("\n")[1]
- temperaturedata = secondline.split(" ")[9]
- temperature = float(temperaturedata[2:]) / 1000
- temperatures.append(temperature)
- if muhconf["main"]["debug"]:
- printem("\t\tSample #{0}: {1}".format(i, temperature))
- avgtemperatures.append(sum(temperatures) / float(len(temperatures)))
- if not muhconf["main"]["dryrun"]:
- if muhconf["main"]["debug"]:
- printem("\t\tInserting average: {0}".format(avgtemperatures[0]))
- cursor.execute("INSERT INTO `{0}` (`{1}`) VALUES ({2})".format(muhconf["mysql"]["table"], muhconf["mysql"]["column"], avgtemperatures[0]))
- db.commit()
- try_db_cleanup(db, cursor)
- except KeyboardInterrupt:
- printem("\nCTRL + C")
- try_db_cleanup(db, cursor)
- sys.exit(RET_GUCCI)
- except SystemExit:
- try_db_cleanup(db, cursor)
- sys.exit(RET_ERR_SENSOR)
- except:
- exc_info = sys.exc_info()
- printem("\nRIP readem_temp() ({0}): {1}".format(exc_info[0], exc_info[1]))
- try_db_cleanup(db, cursor)
- sys.exit(RET_ERR_SENSOR)
- return avgtemperatures
- def checkem_weathermon(inside_temp):
- # Get current state first so that if something goes wrong, we don't spend an API call lel
- statefile = "{0}/weathermon.state".format(skripdir)
- curstate = "outdoors_higher"
- notify = False
- try:
- if os.access(statefile, os.F_OK):
- sfile = open(statefile, "r")
- text = sfile.read().strip() # We'll write the file with a trailing newline, so trim that shit for ez comparison
- sfile.close()
- if text in ["outdoors_higher", "outdoors_lower"]:
- curstate = text
- else:
- if muhconf["main"]["debug"]:
- printem("\tState file exists but contents were unexpected, resetting to '{1}': '{0}'".format(text, curstate))
- except:
- # If we can't read the file we should bail entirely, as we can't reliably tell what to do anymore ;]
- exc_info = sys.exc_info()
- printem("\nRIP checkem_weathermon() state check ({0}): {1}".format(exc_info[0], exc_info[1]))
- sys.exit(RET_ERR_WEATHERMON)
- # Now try the API call imo tbh
- url = "{0}?units=metric&id={1}&appid={2}".format(muhconf["weathermon"]["weather_url"], muhconf["weathermon"]["city_id"], muhconf["weathermon"]["weather_key"])
- if muhconf["main"]["debug"]:
- printem("\tURL: {0}".format(url))
- try:
- req = requests.get(url, verify=muhconf["weathermon"]["tls_ca_file"])
- weather = req.json()
- outside_temp = weather["main"]["temp"]
- if outside_temp > inside_temp:
- if curstate == "outdoors_higher":
- if muhconf["main"]["debug"]:
- printem("\tAyy outside temp ({0}) is still ab0ve inside temp ({1}) br0, not proceeding".format(outside_temp, inside_temp))
- return
- else:
- curstate = "outdoors_higher" # Flip state lol
- if muhconf["main"]["debug"]:
- printem("\tAyy outside temp ({0}) is now ab0ve inside temp ({1}) br0, sending notification".format(outside_temp, inside_temp))
- notify = True
- elif outside_temp < inside_temp:
- if curstate == "outdoors_lower":
- if muhconf["main"]["debug"]:
- printem("\tAyy outside temp ({0}) is still bel0w inside temp ({1}) br0, not proceeding".format(outside_temp, inside_temp))
- return
- else:
- curstate = "outdoors_lower"
- if muhconf["main"]["debug"]:
- printem("\tAyy outside temp ({0}) is now bel0w inside temp ({1}) br0, sending notification".format(outside_temp, inside_temp))
- notify = True
- else:
- # Temps are equal, don't do anything cuz it could go either way lel
- if muhconf["main"]["debug"]:
- printem("\tAyy outside temp ({0}) is equal to inside temp ({1}) br0, not proceeding".format(outside_temp, inside_temp))
- return
- except:
- exc_info = sys.exc_info()
- printem("\nRIP checkem_weathermon() API call ({0}): {1}".format(exc_info[0], exc_info[1]))
- sys.exit(RET_ERR_WEATHERMON)
- # Update state file lol
- try:
- sfile = open(statefile, "w")
- sfile.write("{0}\n".format(curstate))
- sfile.close()
- except:
- # If we can't write the file we should bail entirely, as we can't reliably tell what to do anymore either ;]
- exc_info = sys.exc_info()
- printem("\nRIP checkem_weathermon() state update ({0}): {1}".format(exc_info[0], exc_info[1]))
- sys.exit(RET_ERR_WEATHERMON)
- # We'll send the notification as the very last thing cuz if something would go wrong with updating the state file, we won't keep repeating that shit ;]];;]];;];];]];
- if notify:
- try:
- if curstate == "outdoors_higher":
- msg = "Ayy outside temperature is now ab0ve inside temperature"
- else:
- msg = "Ayy outside temperature is now bel0w inside temperature"
- client = OneSignal(muhconf["weathermon"]["onesignal_restkey"], muhconf["weathermon"]["onesignal_appid"])
- segments = ['All']
- if muhconf["weathermon"]["onesignal_segment"] is not None:
- segments = [muhconf["weathermon"]["onesignal_segment"]]
- req = client.create_notification(
- # Arguments used by the library
- contents=msg,
- heading="Temperature state changed",
- url='',
- included_segments=segments,
- # These are already the defaults but putting that shit here for reference kek
- #app_id=None,
- #player_ids=None, # List or tuple, takes precedence over included_segments
- # Custom args to pass directly to the API
- android_channel_id=muhconf["weathermon"]["onesignal_channel"],
- huawei_channel_id=muhconf["weathermon"]["onesignal_channel"], # Is apparently a separate thing lmao
- )
- if not req.text.startswith("{"):
- printem("Unable to send notification: received invalid JSON response")
- printem("\t{0}".format(req.text))
- sys.exit(RET_ERR_WEATHERMON)
- notification = req.json()
- if req.status_code != 200:
- printem("Unable to send notification: received {0} HTTP status code instead of 200".format(req.status_code))
- printem("\t{0}".format(req.text))
- sys.exit(RET_ERR_WEATHERMON)
- if "errors" in notification and len(notification["errors"]):
- printem("Unable to send notification:")
- for err in notification["errors"]:
- printem("\t{0}".format(err))
- sys.exit(RET_ERR_WEATHERMON)
- if muhconf["main"]["debug"]:
- printem("\t{0}".format(req.text))
- except SystemExit:
- sys.exit(RET_ERR_WEATHERMON)
- except:
- exc_info = sys.exc_info()
- printem("\nRIP checkem_weathermon() notification ({0}): {1}".format(exc_info[0], exc_info[1]))
- sys.exit(RET_ERR_WEATHERMON)
- if __name__ == "__main__":
- dt = datetime.datetime.now()
- weathermon_mins = (dt.hour * 60) + dt.minute
- force_weathermon = False
- # Read config first lol
- readem_conf()
- # Then parse args imo tbh
- p = argparse.ArgumentParser()
- p.add_argument("--debug", help="force debug mode (current setting from config: {0})".format(muhconf["main"]["debug"]), action="store_true")
- p.add_argument("--dryrun", help="do almost everything as usual (connect to SQL, read temperature) but don't actually insert into the database (current setting from config: {0})".format(muhconf["main"]["dryrun"]), action="store_true")
- p.add_argument("--weathermon", help="force running weathermon (current interval from config: {0})".format(muhconf["weathermon"]["interval"]), action="store_true")
- p.add_argument("--version", help="print version and exit lol", action="store_true")
- args, noargs = p.parse_known_args() # noargs contains the 'topkek' in: ./temps.py --debug topkek
- if args.version:
- show_version()
- sys.exit(RET_GUCCI)
- if args.debug:
- muhconf["main"]["debug"] = True
- if args.dryrun:
- muhconf["main"]["dryrun"] = True
- if args.weathermon:
- force_weathermon = True
- if muhconf["main"]["debug"]:
- printem("Reading em temp")
- avgtemperatures = readem_temp()
- if muhconf["weathermon"]["enable"]:
- if len(avgtemperatures) == 0:
- printem("Wanted to run weathermon but avgtemperatures was unexpectedly empty lol")
- sys.exit(RET_ERR_WEATHERMON)
- if force_weathermon:
- if muhconf["main"]["debug"]:
- printem("Ayy forced to run weathermon fam")
- checkem_weathermon(avgtemperatures[0])
- else:
- weathermon_remaining = weathermon_mins % muhconf["weathermon"]["interval"]
- if muhconf["main"]["debug"]:
- printem("Checking if we need to run weathermon: {0} % {1} = {2}".format(weathermon_mins, muhconf["weathermon"]["interval"], weathermon_remaining))
- if weathermon_remaining == 0:
- if muhconf["main"]["debug"]:
- printem("Ayy running weathermon fam")
- checkem_weathermon(avgtemperatures[0])
- else:
- if muhconf["main"]["debug"]:
- printem("Not running weathermon fam")
- sys.exit(RET_GUCCI) # Let's do an explicit exit w/ code imo tbh famlamlaml
|