From e65481482f03aa01892d755fccc78452413e2b13 Mon Sep 17 00:00:00 2001 From: Stephen Herbein Date: Thu, 29 Nov 2018 21:51:55 -0800 Subject: [PATCH 1/4] Add daemon and fake-query scripts --- test/daemon.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++ test/fake-query.py | 48 ++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100755 test/daemon.py create mode 100755 test/fake-query.py diff --git a/test/daemon.py b/test/daemon.py new file mode 100755 index 0000000..f595c1b --- /dev/null +++ b/test/daemon.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +import os +import time +from enum import Enum, auto +from pathlib import PosixPath +import argparse +import subprocess +from collections import deque, namedtuple + +def get_current_weight(): + query_cmd = [str(args.query_exe.resolve())] + if args.query_exe.name == "fake-query.py": + query_cmd.extend(['-p', str(args.pot), '-w', str(args.water), '-t', str(args.tolerance)]) + return float(subprocess.check_output(query_cmd)) + +class ScaleStates(Enum): + NOPOT = "no pot" + EMPTYPOT = "empty pot" + COFFEEPOT = "pot with coffee" + OVERWEIGHT = "overweight" + ERROR = "error" + +def pounds_to_fl_ounces(pounds): + return pounds * 15.34 + +class Reading(): + def __init__(self, weight): + self.weight = weight + + @property + def state(self): + if self.weight < args.tolerance: + return ScaleStates.NOPOT + if (args.pot + args.tolerance) > self.weight > (args.pot - args.tolerance): + return ScaleStates.EMPTYPOT + if (args.pot + args.water + args.tolerance) > self.weight > (args.pot + args.tolerance): + return ScaleStates.COFFEEPOT + if self.weight > (args.pot + args.water + args.tolerance): + return ScaleStates.OVERWEIGHT + return ScaleStates.ERROR + + @property + def state_str(self): + return self.state.value + + def __eq__(self, other): + return self.state_str == other.state_str + + def __str__(self): + if self.state in [ScaleStates.COFFEEPOT, ScaleStates.OVERWEIGHT]: + fl_ounces = pounds_to_fl_ounces(self.weight - args.pot) + return f'{self.state_str} ({fl_ounces:0.1f}ozs)' + else: + return f'{self.state_str}' + +def control_loop(): + past_states = deque(maxlen=2) + while True: + curr_state = Reading(get_current_weight()) + past_states.appendleft(curr_state) + if all([x == curr_state for x in past_states]): + print(f'{curr_state}') + else: + print("Indeterminate state") + time.sleep(args.interval) + +def main(): + control_loop() + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + # TODO: also support reading from stdin + parser.add_argument('query_exe', type=PosixPath, help='path to query executable') + parser.add_argument('-p', '--pot', type=float, default=1.6, + help="weight (in lbs) of the empty coffee pot") + parser.add_argument('-w', '--water', type=float, default=2.6, + help="maximum amount of water (or coffee) that the pot can hold (in lbs)") + parser.add_argument('-t', '--tolerance', type=float, default=0.02, + help="error tolerance (in lbs)") + parser.add_argument('-i', '--interval', type=float, default=2, + help="monitor weight every X seconds") + args = parser.parse_args() + + main() diff --git a/test/fake-query.py b/test/fake-query.py new file mode 100755 index 0000000..60af6c2 --- /dev/null +++ b/test/fake-query.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +from __future__ import print_function + +import time +import random +import argparse +import itertools + +def get_weight(pot, water, fudge_factor): + if random.random() < 0.5: + return random.uniform(pot-fudge_factor, pot+water+fudge_factor) + else: + return random.uniform(0, fudge_factor) + +def output_weight(weight): + print(f"{weight:.04f}") + +def inf(): + return iter(lambda:0,1) + +def main(): + random.seed() + + if args.interval: + weights = (get_weight(args.pot, args.water, args.tolerance) for x in inf()) + if args.max_iters: + weights = itertools.islice(weights, 0, args.max_iters - 1) + for weight in weights: + output_weight(weight) + else: + output_weight(get_weight(args.pot, args.water, args.tolerance)) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('-i', '--interval', type=float, + help="continually output weight every X seconds") + parser.add_argument('-m', '--max-iters', type=int, + help="maximum number of weights to output") + parser.add_argument('-w', '--water', type=float, default=2.6, + help="maximum amount of water (or coffee) that the pot can hold (in lbs)") + parser.add_argument('-p', '--pot', type=float, default=1.6, + help="weight (in lbs) of the empty coffee pot") + parser.add_argument('-t', '--tolerance', type=float, default=0.02, + help="error tolerance (in lbs)") + args = parser.parse_args() + + main() From 084e6e6eb2425be1439bf5b714d6a85377591593 Mon Sep 17 00:00:00 2001 From: Stephen Herbein Date: Fri, 30 Nov 2018 10:14:34 -0800 Subject: [PATCH 2/4] python 3.5 compatibility --- test/daemon.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/daemon.py b/test/daemon.py index f595c1b..7049ef1 100755 --- a/test/daemon.py +++ b/test/daemon.py @@ -2,7 +2,7 @@ import os import time -from enum import Enum, auto +from enum import Enum from pathlib import PosixPath import argparse import subprocess @@ -50,9 +50,9 @@ def __eq__(self, other): def __str__(self): if self.state in [ScaleStates.COFFEEPOT, ScaleStates.OVERWEIGHT]: fl_ounces = pounds_to_fl_ounces(self.weight - args.pot) - return f'{self.state_str} ({fl_ounces:0.1f}ozs)' + return "{} ({})ozs".format(self.state_str, fl_ounces) else: - return f'{self.state_str}' + return '{}'.format(self.state_str) def control_loop(): past_states = deque(maxlen=2) @@ -60,7 +60,7 @@ def control_loop(): curr_state = Reading(get_current_weight()) past_states.appendleft(curr_state) if all([x == curr_state for x in past_states]): - print(f'{curr_state}') + print('{}'.format(curr_state)) else: print("Indeterminate state") time.sleep(args.interval) From 98fdbef5fbcd430df114baf83afa58c96364af9d Mon Sep 17 00:00:00 2001 From: Stephen Herbein Date: Fri, 30 Nov 2018 10:19:02 -0800 Subject: [PATCH 3/4] py3.5 compat --- test/fake-query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fake-query.py b/test/fake-query.py index 60af6c2..699e083 100755 --- a/test/fake-query.py +++ b/test/fake-query.py @@ -14,7 +14,7 @@ def get_weight(pot, water, fudge_factor): return random.uniform(0, fudge_factor) def output_weight(weight): - print(f"{weight:.04f}") + print("{:.04f}".format(weight)) def inf(): return iter(lambda:0,1) From 1bf8d06930d2a4081bd6e13bad81ccf1dd47056f Mon Sep 17 00:00:00 2001 From: Stephen Herbein Date: Fri, 15 Mar 2019 09:31:19 -0700 Subject: [PATCH 4/4] all the changes! --- test/daemon.py | 81 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/test/daemon.py b/test/daemon.py index 7049ef1..ff03d01 100755 --- a/test/daemon.py +++ b/test/daemon.py @@ -2,17 +2,57 @@ import os import time +from datetime import datetime from enum import Enum from pathlib import PosixPath import argparse import subprocess -from collections import deque, namedtuple +import random +from collections import deque, namedtuple, defaultdict + +from slackclient import SlackClient + +class WeightNotStableError(RuntimeError): + pass def get_current_weight(): query_cmd = [str(args.query_exe.resolve())] if args.query_exe.name == "fake-query.py": query_cmd.extend(['-p', str(args.pot), '-w', str(args.water), '-t', str(args.tolerance)]) - return float(subprocess.check_output(query_cmd)) + subproc = subprocess.Popen(query_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = subproc.communicate() + if stderr and len(stderr) > 0: + try: + stderr = stderr.decode('utf-8') + except UnicodeDecodeError: + raise RuntimeError("Failed to decode stderr: {}".format(stderr.hex())) + if "Over capacity" in stderr: + return float("inf") + elif "Under capacity" in stderr: + return 0.0 + elif "Weight not stable" in stderr: + raise WeightNotStableError() + else: + raise RuntimeError() + return float(stdout) + +def get_notification_template(): + return random.choice([ + "{:.1f} ounces of hot, fresh coffee are ready for consumption in B451.", + "Please drink the {:.1f} ounces of coffee in B451. You have 20 seconds to comply.", + "You must consume the {:.1f} ounces of coffee or be terminated.", + "{:.1f} ounces of human productivity beverage available for consumption in B451", + ]) + +def send_slack_notification(weight_ounces, channel="#flux"): + now = datetime.now() + if ((now - send_slack_notification.last_notify[channel]).total_seconds() > 60 * 60): + message_template = get_notification_template() + message = message_template.format(weight_ounces) + print("Sending slack notification: {}".format(message)) + sc.api_call("chat.postMessage", channel=channel, text=message) + send_slack_notification.last_notify[channel] = now +send_slack_notification.last_notify = defaultdict(lambda: datetime.fromtimestamp(0)) class ScaleStates(Enum): NOPOT = "no pot" @@ -56,23 +96,33 @@ def __str__(self): def control_loop(): past_states = deque(maxlen=2) + slack_message_already_sent = False while True: - curr_state = Reading(get_current_weight()) - past_states.appendleft(curr_state) - if all([x == curr_state for x in past_states]): - print('{}'.format(curr_state)) - else: - print("Indeterminate state") + try: + curr_state = Reading(get_current_weight()) + past_states.appendleft(curr_state) + if all([x == curr_state for x in past_states]): + print('{}'.format(curr_state)) + if curr_state.state == ScaleStates.COFFEEPOT: + fl_ounces = pounds_to_fl_ounces(curr_state.weight - args.pot) + if not slack_message_already_sent: + send_slack_notification(fl_ounces) + slack_message_already_sent = True + else: + slack_message_already_sent = False + else: + print("Indeterminate state") + except WeightNotStableError: + pass + except RuntimeError as e: + print(e) time.sleep(args.interval) -def main(): - control_loop() - if __name__ == "__main__": parser = argparse.ArgumentParser() # TODO: also support reading from stdin parser.add_argument('query_exe', type=PosixPath, help='path to query executable') - parser.add_argument('-p', '--pot', type=float, default=1.6, + parser.add_argument('-p', '--pot', type=float, default=1.75, help="weight (in lbs) of the empty coffee pot") parser.add_argument('-w', '--water', type=float, default=2.6, help="maximum amount of water (or coffee) that the pot can hold (in lbs)") @@ -82,4 +132,9 @@ def main(): help="monitor weight every X seconds") args = parser.parse_args() - main() + random.seed() + + slack_token = os.environ["SLACK_API_TOKEN"] + sc = SlackClient(slack_token) + + control_loop()