diff --git a/README.md b/README.md index 814ce76..aeb3ad9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

MudPi Smart Garden

# MudPi Smart Garden -> A python library to gather sensor readings, trigger components, control solenoids and more in an event based system that can be run on a raspberry pi. +> A python library to gather sensor readings, trigger components, control solenoids and more in an event based system that can be run on a linux SBC, including Raspberry Pi. ## Documentation @@ -17,7 +17,7 @@ For examples and guides on how to setup and use MudPi check out the [free guides ## Contributing -Any contributions you can make will be greatly appreciated. If you are interested in contributing please get in touch with me and submit a pull request. There is much more I would like to add support for, however being a single developer limits my scope. Therefore mainly bugs will be accepted as issues. +Any contributions you can make will be greatly appreciated. If you are interested in contributing please get in touch with me and submit a pull request. There is much more I would like to add support for, however being a single developer limits my scope. Therefore mainly bugs will be accepted as issues. ## Versioning @@ -33,7 +33,7 @@ Breaking.Major.Minor * [Twitter.com/MudpiApp](https://twitter.com/mudpiapp) ## Hardware Tested On -These are the devices and sensors I tested and used with MudPi successfully. Many sensors are similar so it will work with a range more than what is listed below. +These are the devices and sensors I tested and used with MudPi successfully. Many sensors are similar so it will work with a range more than what is listed below. **Devices** * [Raspberry Pi 2 Model B+](https://www.raspberrypi.org/products/raspberry-pi-2-model-b/) @@ -53,6 +53,7 @@ These are the devices and sensors I tested and used with MudPi successfully. Man * [DS18B20 Temperature Sensors](https://www.amazon.com/gp/product/B018KFX5X0/ref=oh_aui_detailpage_o08_s00?ie=UTF8&psc=1) * [DHT11 Temperature/Humidity Sensor](https://www.amazon.com/gp/product/B01DKC2GQ0/ref=oh_aui_detailpage_o07_s05?ie=UTF8&psc=1) * [DHT22 Temperature/Humidity Sensor](https://www.dfrobot.com/product-1102.html) +* [T9602 Temperature/Humidity Sensor](https://www.amphenol-sensors.com/en/telaire/humidity/527-humidity-sensors/3224-t9602) * [Rain Sensor](https://www.amazon.com/gp/product/B01D9JK2F6/ref=oh_aui_detailpage_o03_s00?ie=UTF8&psc=1) * [Ambient Light Sensor](https://www.dfrobot.com/product-1004.html) * [DFROBOT Analog Capacitive Soil Moisture Sensor](https://www.amazon.com/gp/product/B01GHY0N4K/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1) @@ -60,6 +61,7 @@ These are the devices and sensors I tested and used with MudPi successfully. Man **Components** * [4 Channel DC 5V Relay](https://www.amazon.com/gp/product/B00KTEN3TM/ref=oh_aui_detailpage_o08_s00?ie=UTF8&psc=1) +* [8 Channel DC 5V Relay](https://www.amazon.co.uk/SODIAL-Channel-Module-Arduino-Electronic/dp/B00IIDXYTA) * [LCD 16 x 2 Display I2C](https://www.dfrobot.com/product-135.html) * [LCD 16 x 2 Display I2C - PCF8574](https://www.amazon.com/Arducam-Display-Controller-Character-Backlight/dp/B019D9TYMI) * [LCD 20 x 4 Display I2C](https://www.dfrobot.com/product-590.html) @@ -74,4 +76,3 @@ This project is licensed under the BSD-4-Clause License - see the [LICENSE.md](L MudPi Smart Garden - diff --git a/action.py b/action.py index 865855a..bbacbcd 100644 --- a/action.py +++ b/action.py @@ -1,45 +1,54 @@ -import time import json -import redis import subprocess import sys -sys.path.append('..') + +import redis + + + class Action(): - def __init__(self, config): - self.config = config - self.name = config.get("name", "Action") - self.type = config.get("type", "event") - self.key = config.get("key", None).replace(" ", "_").lower() if config.get("key") is not None else self.name.replace(" ", "_").lower() - # Actions will be either objects to publish for events or a command string to execute - self.action = config.get("action") - try: - self.r = config["redis"] if config["redis"] is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - return - - def init_action(self): - if self.type == 'event': - self.topic = self.config.get("topic", "mudpi") - elif self.type == 'command': - self.shell = self.config.get("shell", False) - - def trigger(self, value=None): - if self.type == 'event': - self.emitEvent() - elif self.type == 'command': - self.runCommand(value) - return - - def emitEvent(self): - self.r.publish(self.topic, json.dumps(self.action)) - return - - def runCommand(self, value=None): - if value is None: - completed_process = subprocess.run([self.action], shell=self.shell) - else: - completed_process = subprocess.run([self.action, json.dumps(value)], shell=self.shell) - return \ No newline at end of file + def __init__(self, config): + self.config = config + self.name = config.get("name", "Action") + self.type = config.get("type", "event") + self.key = config.get("key", None).replace(" ", + "_").lower() if config.get( + "key") is not None else self.name.replace(" ", "_").lower() + # Actions will be either objects to publish for events + # or a command string to execute + self.action = config.get("action") + + try: + self.r = config["redis"] if config[ + "redis"] is not None else redis.Redis( + host='127.0.0.1', port=6379) + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + return + + def init_action(self): + if self.type == 'event': + self.topic = self.config.get("topic", "mudpi") + elif self.type == 'command': + self.shell = self.config.get("shell", False) + + def trigger(self, value=None): + if self.type == 'event': + self.emit_event() + elif self.type == 'command': + self.run_command(value) + return + + def emit_event(self): + self.r.publish(self.topic, json.dumps(self.action)) + return + + def run_command(self, value=None): + if value is None: + completed_process = subprocess.run([self.action], shell=self.shell) + else: + completed_process = subprocess.run( + [self.action, json.dumps(value)], shell=self.shell) + return diff --git a/config_load.py b/config_load.py deleted file mode 100644 index 11b7774..0000000 --- a/config_load.py +++ /dev/null @@ -1,8 +0,0 @@ -import json - -def loadConfigJson(): - configs = {} - with open('mudpi.config') as loadedfile: - configs = json.load(loadedfile) - loadedfile.close() - return configs \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..c33e7ee --- /dev/null +++ b/constants.py @@ -0,0 +1,5 @@ +PREVIOUS_LINE = "\x1b[1F" +RED_BACK = "\x1b[41;37m" +GREEN_BACK = "\x1b[42;30m" +YELLOW_BACK = "\x1b[43;30m" +RESET = "\x1b[0m" diff --git a/controls/arduino/button_control.py b/controls/arduino/button_control.py index cab3c1c..c0cab0d 100644 --- a/controls/arduino/button_control.py +++ b/controls/arduino/button_control.py @@ -10,30 +10,30 @@ class ButtonControl(Control): - def __init__(self, pin, name=None, key=None, connection=default_connection, analog_pin_mode=False, topic=None, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, analog_pin_mode=analog_pin_mode, redis_conn=redis_conn) - self.topic = topic.replace(" ", "/").lower() if topic is not None else 'mudpi/controls/'+self.key - self.state_counter = 3 - self.previous_state = 0 - return + def __init__(self, pin, name=None, key=None, connection=default_connection, analog_pin_mode=False, topic=None, redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, analog_pin_mode=analog_pin_mode, redis_conn=redis_conn) + self.topic = topic.replace(" ", "/").lower() if topic is not None else 'mudpi/controls/'+self.key + self.state_counter = 3 + self.previous_state = 0 + return - def init_control(self): - super().init_control() + def init_control(self): + super().init_control() - def read(self): - state = super().read() - if state == self.previous_state: - self.state_counter += 1 - if self.state_counter % 2 == 0: - if state: - super().emitEvent(1) - elif self.state_counter == 2: - super().emitEvent(0) - else: - self.state_counter = 1 + def read(self): + state = super().read() + if state == self.previous_state: + self.state_counter += 1 + if self.state_counter % 2 == 0: + if state: + super().emitEvent(1) + elif self.state_counter == 2: + super().emitEvent(0) + else: + self.state_counter = 1 - self.previous_state = state - return state + self.previous_state = state + return state - def readRaw(self): - return super().read() + def read_raw(self): + return super().read() diff --git a/controls/arduino/control.py b/controls/arduino/control.py index 77a45ef..a7eae44 100644 --- a/controls/arduino/control.py +++ b/controls/arduino/control.py @@ -3,59 +3,59 @@ import redis from nanpy import (ArduinoApi, SerialManager) import sys -sys.path.append('..') + default_connection = SerialManager() # Base sensor class to extend all other arduino sensors from. class Control(): - def __init__(self, pin, name=None, connection=default_connection, analog_pin_mode=False, key=None, redis_conn=None): - self.pin = pin - - if key is None: - raise Exception('No "key" Found in Control Config') - else: - self.key = key.replace(" ", "_").lower() - - if name is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = name - - self.analog_pin_mode = analog_pin_mode - self.connection = connection - self.api = ArduinoApi(connection) - try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - return - - def init_control(self): - #Initialize the control here (i.e. set pin mode, get addresses, etc) - self.api.pinMode(self.pin, self.api.INPUT) - pass - - def read(self): - #Read the sensor(s), parse the data and store it in redis if redis is configured - return self.readPin() - - def readRaw(self): - #Read the sensor(s) but return the raw data, useful for debugging - pass - - def readPin(self): - #Read the pin from the ardiuno. Can be analog or digital based on "analog_pin_mode" - data = self.api.analogRead(self.pin) if self.analog_pin_mode else self.api.digitalRead(self.pin) - return data - - def emitEvent(self, data): - message = { - 'event':'ControlUpdate', - 'data': { - self.key:data - } - } - print(message["data"]) - self.r.publish('controls', json.dumps(message)) \ No newline at end of file + def __init__(self, pin, name=None, connection=default_connection, analog_pin_mode=False, key=None, redis_conn=None): + self.pin = pin + + if key is None: + raise Exception('No "key" Found in Control Config') + else: + self.key = key.replace(" ", "_").lower() + + if name is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = name + + self.analog_pin_mode = analog_pin_mode + self.connection = connection + self.api = ArduinoApi(connection) + try: + self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + return + + def init_control(self): + #Initialize the control here (i.e. set pin mode, get addresses, etc) + self.api.pinMode(self.pin, self.api.INPUT) + pass + + def read(self): + #Read the sensor(s), parse the data and store it in redis if redis is configured + return self.read_pin() + + def read_raw(self): + #Read the sensor(s) but return the raw data, useful for debugging + pass + + def read_pin(self): + #Read the pin from the ardiuno. Can be analog or digital based on "analog_pin_mode" + data = self.api.analogRead(self.pin) if self.analog_pin_mode else self.api.digitalRead(self.pin) + return data + + def emitEvent(self, data): + message = { + 'event':'ControlUpdate', + 'data': { + self.key:data + } + } + print(message["data"]) + self.r.publish('controls', json.dumps(message)) \ No newline at end of file diff --git a/controls/arduino/potentiometer_control.py b/controls/arduino/potentiometer_control.py index f0d26f7..dbf6d05 100644 --- a/controls/arduino/potentiometer_control.py +++ b/controls/arduino/potentiometer_control.py @@ -10,29 +10,29 @@ class PotentiometerControl(Control): - def __init__(self, pin, name=None, key=None, connection=default_connection, analog_pin_mode=True, topic=None, reading_buffer=3, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, analog_pin_mode=analog_pin_mode, redis_conn=redis_conn) - self.previous_state = 0 - # Reading buffer helps prevent multiple events when values are floating between small amounts - self.reading_buffer = reading_buffer - return - - def init_control(self): - super().init_control() - # Set initial state to prevent event on boot - self.previous_state = super().read() - - def read(self): - state = super().read() - - if (state < self.previous_state - self.reading_buffer) or (state > self.previous_state + self.reading_buffer): - # Value changed - # print('{0}: {1}'.format(self.name, state)) - super().emitEvent(state) - - self.previous_state = state - return state - - def readRaw(self): - return super().read() + def __init__(self, pin, name=None, key=None, connection=default_connection, analog_pin_mode=True, topic=None, reading_buffer=3, redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, analog_pin_mode=analog_pin_mode, redis_conn=redis_conn) + self.previous_state = 0 + # Reading buffer helps prevent multiple events when values are floating between small amounts + self.reading_buffer = reading_buffer + return + + def init_control(self): + super().init_control() + # Set initial state to prevent event on boot + self.previous_state = super().read() + + def read(self): + state = super().read() + + if (state < self.previous_state - self.reading_buffer) or (state > self.previous_state + self.reading_buffer): + # Value changed + # print('{0}: {1}'.format(self.name, state)) + super().emitEvent(state) + + self.previous_state = state + return state + + def read_raw(self): + return super().read() diff --git a/controls/arduino/switch_control.py b/controls/arduino/switch_control.py index 972d01c..2b4eb45 100644 --- a/controls/arduino/switch_control.py +++ b/controls/arduino/switch_control.py @@ -10,31 +10,31 @@ class SwitchControl(Control): - def __init__(self, pin, name=None, key=None, connection=default_connection, analog_pin_mode=False, topic=None, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, analog_pin_mode=analog_pin_mode, redis_conn=redis_conn) - self.topic = topic.replace(" ", "/").lower() if topic is not None else 'mudpi/controls/'+self.key - self.state_counter = 3 - self.previous_state = 0 - self.trigger_delay = 2 - return + def __init__(self, pin, name=None, key=None, connection=default_connection, analog_pin_mode=False, topic=None, redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, analog_pin_mode=analog_pin_mode, redis_conn=redis_conn) + self.topic = topic.replace(" ", "/").lower() if topic is not None else 'mudpi/controls/'+self.key + self.state_counter = 3 + self.previous_state = 0 + self.trigger_delay = 2 + return - def init_control(self): - super().init_control() - # Set initial state to prevent event on boot - self.previous_state = super().read() + def init_control(self): + super().init_control() + # Set initial state to prevent event on boot + self.previous_state = super().read() - def read(self): - state = super().read() - if state == self.previous_state: - self.state_counter += 1 - if self.state_counter == self.trigger_delay: - clean_state = 1 if state else 0 - super().emitEvent(clean_state) - else: - self.state_counter = 1 + def read(self): + state = super().read() + if state == self.previous_state: + self.state_counter += 1 + if self.state_counter == self.trigger_delay: + clean_state = 1 if state else 0 + super().emitEvent(clean_state) + else: + self.state_counter = 1 - self.previous_state = state - return state + self.previous_state = state + return state - def readRaw(self): - return super().read() + def read_raw(self): + return super().read() diff --git a/controls/pi/__init__.py b/controls/linux/__init__.py similarity index 100% rename from controls/pi/__init__.py rename to controls/linux/__init__.py diff --git a/controls/linux/button_control.py b/controls/linux/button_control.py new file mode 100644 index 0000000..db48cfb --- /dev/null +++ b/controls/linux/button_control.py @@ -0,0 +1,30 @@ +import time +import datetime +import json +import redis +from .control import Control + + +r = redis.Redis(host='127.0.0.1', port=6379) + + +class ButtonControl(Control): + + def __init__(self, pin, name=None, key=None, resistor=None, edge_detection=None, debounce=None, topic=None, redis_conn=None): + super().__init__(pin, name=name, key=key, resistor=resistor, edge_detection=edge_detection, debounce=debounce, redis_conn=redis_conn) + self.topic = topic.replace(" ", "/").lower() if topic is not None else 'mudpi/controls/'+self.key + return + + def init_control(self): + super().init_control() + + def read(self): + state = super().read() + if state: + # Button Pressed + # eventually add multipress tracking + super().emitEvent(1) + return state + + def read_raw(self): + return super().read() diff --git a/controls/linux/control.py b/controls/linux/control.py new file mode 100644 index 0000000..b7339ce --- /dev/null +++ b/controls/linux/control.py @@ -0,0 +1,99 @@ +import time +import json +import redis +import sys +import board +import digitalio +from adafruit_debouncer import Debouncer + + + + + +# Base sensor class to extend all other arduino sensors from. +class Control(): + + def __init__(self, pin, name=None, key=None, resistor=None, edge_detection=None, debounce=None, redis_conn=None): + self.pin_obj = getattr(board, pin) + + if key is None: + raise Exception('No "key" Found in Control Config') + else: + self.key = key.replace(" ", "_").lower() + + if name is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = name + + self.gpio = digitalio + self.debounce = debounce if debounce is not None else None + try: + self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + + if resistor is not None: + if resistor == "up" or resistor == digitalio.Pull.UP: + self.resistor = digitalio.Pull.UP + elif resistor == "down" or resistor == digitalio.Pull.DOWN: + self.resistor = digitalio.Pull.DOWN + else: + self.resistor = resistor + + if edge_detection is not None: + if edge_detection == "falling" or edge_detection == "fell": + self.edge_detection = "fell" + elif edge_detection == "rising" or edge_detection == "rose": + self.edge_detection = "rose" + elif edge_detection == "both": + self.edge_detection = "both" + else: + self.edge_detection = None + + return + + def init_control(self): + """Initialize the control here (i.e. set pin mode, get addresses, etc) + Set the Pin for the button with the internal pull up resistor""" + self.control_pin = self.gpio.DigitalInOut(self.pin_obj) + self.control_pin.switch_to_input(pull=self.resistor) + # If edge detection has been configured lets take advantage of that + if self.edge_detection is not None: + self.button = Debouncer(self.control_pin) + if self.debounce is not None: + self.button.interval = self.debounce + + def read(self): + """Read the sensor(s), parse the data and store it in redis if redis is configured + If edge detection is being used return the detection event instead""" + if self.edge_detection is not None: + self.button.update() + if self.edge_detection == "both": + if self.button.fell or self.button.rose: + return True + else: + return False + else: # "fell" or "rose" + return getattr(self.button, self.edge_detection) + else: + return None + + def read_raw(self): + """Read the sensor(s) but return the raw data, useful for debugging""" + pass + + def read_pin(self): + """Read the pin from the board digital reads only""" + data = self.gpio.DigitalInOut(self.pin_obj).value + return data + + def emitEvent(self, data): + message = { + 'event': 'ControlUpdate', + 'data': { + self.key: data + } + } + print(message["data"]) + self.r.publish('controls', json.dumps(message)) diff --git a/controls/linux/switch_control.py b/controls/linux/switch_control.py new file mode 100644 index 0000000..154585a --- /dev/null +++ b/controls/linux/switch_control.py @@ -0,0 +1,42 @@ +import time +import datetime +import json +import redis +from .control import Control + + +r = redis.Redis(host='127.0.0.1', port=6379) + + +class SwitchControl(Control): + + def __init__(self, pin, name=None, key=None, resistor=None, edge_detection=None, debounce=None, topic=None, redis_conn=None): + super().__init__(pin, name=name, key=key, resistor=resistor, edge_detection=edge_detection, debounce=debounce, redis_conn=redis_conn) + self.topic = topic.replace(" ", "/").lower() if topic is not None else 'mudpi/controls/'+self.key + # Keep counter 1 above delay to avoid event on boot + self.state_counter = 3 + self.previous_state = 0 + self.trigger_delay = 2 + return + + def init_control(self): + super().init_control() + # Get current state on boot + self.previous_state = super().read() + + def read(self): + state = super().read() + if state == self.previous_state: + self.state_counter += 1 + if self.state_counter == self.trigger_delay: + clean_state = 1 if state else 0 + super().emitEvent(clean_state) + else: + # Button State Changed + self.state_counter = 1 + + self.previous_state = state + return state + + def read_raw(self): + return super().read() diff --git a/controls/pi/button_control.py b/controls/pi/button_control.py deleted file mode 100644 index f79075a..0000000 --- a/controls/pi/button_control.py +++ /dev/null @@ -1,30 +0,0 @@ -import time -import datetime -import json -import redis -from .control import Control -import RPi.GPIO as GPIO - -r = redis.Redis(host='127.0.0.1', port=6379) - -class ButtonControl(Control): - - def __init__(self, pin, name=None, key=None, resistor=None, edge_detection=None, debounce=None, topic=None, redis_conn=None): - super().__init__(pin, name=name, key=key, resistor=resistor, edge_detection=edge_detection, debounce=debounce, redis_conn=redis_conn) - self.topic = topic.replace(" ", "/").lower() if topic is not None else 'mudpi/controls/'+self.key - return - - def init_control(self): - super().init_control() - - def read(self): - state = super().read() - if state: - #Button Pressed - #eventually add multipress tracking - super().emitEvent(1) - return state - - def readRaw(self): - return super().read() - diff --git a/controls/pi/control.py b/controls/pi/control.py deleted file mode 100644 index fc5c35f..0000000 --- a/controls/pi/control.py +++ /dev/null @@ -1,82 +0,0 @@ -import time -import json -import redis -import RPi.GPIO as GPIO -import sys -sys.path.append('..') - -# Base sensor class to extend all other arduino sensors from. -class Control(): - - def __init__(self, pin, name=None, key=None, resistor=None, edge_detection=None, debounce=None, redis_conn=None): - self.pin = pin - - if key is None: - raise Exception('No "key" Found in Control Config') - else: - self.key = key.replace(" ", "_").lower() - - if name is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = name - - self.gpio = GPIO - self.debounce = debounce if debounce is not None else None - try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - - if resistor is not None: - if resistor == "up" or resistor == GPIO.PUD_UP: - self.resistor = GPIO.PUD_UP - elif resistor == "down" or resistor == GPIO.PUD_DOWN: - self.resistor = GPIO.PUD_DOWN - else: - self.resistor = resistor - - if edge_detection is not None: - if edge_detection == "falling" or edge_detection == GPIO.FALLING: - self.edge_detection = GPIO.FALLING - elif edge_detection == "rising" or edge_detection == GPIO.RISING: - self.edge_detection = GPIO.RISING - elif edge_detection == "both" or edge_detection == GPIO.BOTH: - self.edge_detection = GPIO.BOTH - else: - self.edge_detection = None - - return - - def init_control(self): - #Initialize the control here (i.e. set pin mode, get addresses, etc) - #Set the Pin for the button with the internal pull up resistor - self.gpio.setup(self.pin, GPIO.IN, pull_up_down=self.resistor) - # If edge detection has been configured lets take advantage of that - if self.edge_detection is not None: - GPIO.add_event_detect(self.pin, self.edge_detection, bouncetime=self.debounce) - pass - - def read(self): - #Read the sensor(s), parse the data and store it in redis if redis is configured - #If edge detection is being used return the detection event instead - return self.readPin() if self.edge_detection is None else GPIO.event_detected(self.pin) - - def readRaw(self): - #Read the sensor(s) but return the raw data, useful for debugging - pass - - def readPin(self): - #Read the pin from the pi digital reads only - data = self.gpio.input(self.pin) - return data - - def emitEvent(self, data): - message = { - 'event':'ControlUpdate', - 'data': { - self.key:data - } - } - print(message["data"]) - self.r.publish('controls', json.dumps(message)) \ No newline at end of file diff --git a/controls/pi/switch_control.py b/controls/pi/switch_control.py deleted file mode 100644 index e2ffc21..0000000 --- a/controls/pi/switch_control.py +++ /dev/null @@ -1,42 +0,0 @@ -import time -import datetime -import json -import redis -from .control import Control -import RPi.GPIO as GPIO - -r = redis.Redis(host='127.0.0.1', port=6379) - -class SwitchControl(Control): - - def __init__(self, pin, name=None, key=None, resistor=None, edge_detection=None, debounce=None, topic=None, redis_conn=None): - super().__init__(pin, name=name, key=key, resistor=resistor, edge_detection=edge_detection, debounce=debounce, redis_conn=redis_conn) - self.topic = topic.replace(" ", "/").lower() if topic is not None else 'mudpi/controls/'+self.key - # Keep counter 1 above delay to avoid event on boot - self.state_counter = 3 - self.previous_state = 0 - self.trigger_delay = 2 - return - - def init_control(self): - super().init_control() - # Get current state on boot - self.previous_state = super().read() - - def read(self): - state = super().read() - if state == self.previous_state: - self.state_counter += 1 - if self.state_counter == self.trigger_delay: - clean_state = 1 if state else 0 - super().emitEvent(clean_state) - else: - #Button State Changed - self.state_counter = 1 - - self.previous_state = state - return state - - def readRaw(self): - return super().read() - diff --git a/logger/Logger.py b/logger/Logger.py index da37cc0..3d83379 100644 --- a/logger/Logger.py +++ b/logger/Logger.py @@ -1,7 +1,6 @@ import logging import sys - LOG_LEVEL = { "unknown": logging.NOTSET, "debug": logging.DEBUG, @@ -11,8 +10,8 @@ "error": logging.ERROR, } -class Logger: +class Logger: logger = None def __init__(self, config: dict): @@ -25,24 +24,28 @@ def __init__(self, config: dict): "terminal_log_level": "info" } # raise Exception("No Logger configs were found!") - - self.__log = logging.getLogger(config['name']+"_stream") + + self.__log = logging.getLogger(config['name'] + "_stream") self.__file_log = logging.getLogger(config['name']) try: - file_log_level = LOG_LEVEL[logger_config["file_log_level"]] if not config["debug"] else LOG_LEVEL["debug"] - stream_log_level = LOG_LEVEL[logger_config["terminal_log_level"]] if not config["debug"] else LOG_LEVEL["debug"] + file_log_level = LOG_LEVEL[logger_config["file_log_level"]] if not \ + config["debug"] else LOG_LEVEL["debug"] + stream_log_level = LOG_LEVEL[ + logger_config["terminal_log_level"]] if not config[ + "debug"] else LOG_LEVEL["debug"] except KeyError: file_log_level = LOG_LEVEL["unknown"] stream_log_level = LOG_LEVEL["unknown"] - + self.__log.setLevel(stream_log_level) self.__file_log.setLevel(file_log_level) try: try: if len(logger_config['file']) != 0: - open(logger_config['file'], 'w').close() # testing file path + open(logger_config['file'], + 'w').close() # testing file path file_handler = logging.FileHandler(logger_config['file']) file_handler.setLevel(file_log_level) @@ -51,16 +54,19 @@ def __init__(self, config: dict): self.WRITE_TO_FILE = False except FileNotFoundError: self.WRITE_TO_FILE = False - + except KeyError as e: self.WRITE_TO_FILE = False - self.log(LOG_LEVEL["warning"], "File Handler could not be started due to a KeyError: {0}".format(e)) - + self.log(LOG_LEVEL["warning"], + "File Handler could not be started due to a KeyError: {0}".format( + e)) + stream_handler = logging.StreamHandler(sys.stdout) stream_handler.setLevel(stream_log_level) - file_formatter = logging.Formatter("[%(asctime)s][%(name)s][%(levelname)s] %(message)s", "%H:%M:%S") + file_formatter = logging.Formatter( + "[%(asctime)s][%(name)s][%(levelname)s] %(message)s", "%H:%M:%S") stream_formatter = logging.Formatter("%(message)s") file_handler.setFormatter(file_formatter) stream_handler.setFormatter(stream_formatter) @@ -68,7 +74,7 @@ def __init__(self, config: dict): if self.WRITE_TO_FILE: self.__file_log.addHandler(file_handler) self.__log.addHandler(stream_handler) - + @staticmethod def log_to_file(log_level: int, msg: str): """ @@ -76,7 +82,7 @@ def log_to_file(log_level: int, msg: str): """ if Logger.log is not None: Logger.logger.log_this_file(log_level, msg) - + @staticmethod def log(log_level: int, msg: str): # for ease of access from outside """ @@ -94,7 +100,7 @@ def log_this_file(self, log_level: int, msg: str): msg = msg.replace("\033[1;31m", "") msg = msg.replace("\033[0;0m", "") self.__file_log.log(log_level, msg) - + def log_this(self, log_level: int, msg: str): self.__log.log(log_level, msg) if self.WRITE_TO_FILE: diff --git a/mudpi.config.example b/mudpi.config.example index 961e877..7978fee 100644 --- a/mudpi.config.example +++ b/mudpi.config.example @@ -16,27 +16,27 @@ }, "relays": [ { - "pin": 13, + "pin": "D13", "normally_open": true, "group": "1", "name": "Relay Name", - "topic": "garden/pi/relays/1", + "topic": "garden/linux/relays/1", "key": "relay_1" }, { - "pin": 12, + "pin": "D12", "normally_open": true, "group": "2", "name": "Relay Name 2", - "topic": "garden/pi/relays/2", + "topic": "garden/linux/relays/2", "key": "relay_2" }, { - "pin": 20, + "pin": "D20", "normally_open": true, "group": "2", "name": "Relay Name 3", - "topic": "garden/pi/relays/3", + "topic": "garden/linux/relays/3", "key": "relay_3" } ], @@ -47,11 +47,11 @@ "channel":"controls", "controls": [ { - "pin":18, + "pin":"D18", "type":"button", "name":"Button 1", "key": "button_1", - "topic": "garden/pi/relays/1", + "topic": "garden/linux/relays/1", "action":"Toggle", "resistor":"up", "edge_detection":"falling", @@ -68,7 +68,7 @@ "use_wifi":true, "sensors": [ { - "pin": 2, + "pin": "D2", "is_digital": false, "type": "Humidity", "name": "Weather" @@ -76,18 +76,18 @@ ], "controls": [ { - "pin":16, + "pin": 16, "type":"button", "name":"Button 1", "key": "button_1", - "topic": "garden/pi/relays/1" + "topic": "garden/linux/relays/1" }, { "pin":5, "type":"switch", "name":"Switch 1", "key": "switch_1", - "topic": "garden/pi/relays/1" + "topic": "garden/linux/relays/1" }, { "pin":0, @@ -153,7 +153,7 @@ "name": "Turn on Lights", "key": "turn_on_lights_1", "action": {"event":"Toggle"}, - "topic": "garden/pi/relays/2" + "topic": "garden/linux/relays/2" } ] } diff --git a/mudpi.py b/mudpi.py old mode 100755 new mode 100644 index 14ef29d..bc28e4c --- a/mudpi.py +++ b/mudpi.py @@ -1,45 +1,56 @@ -import RPi.GPIO as GPIO -import threading +"""MudPi Core +Author: Eric Davisson (@theDavisson) [EricDavisson.com] +https://mudpi.app +MudPi Core is a python library to gather sensor readings, +control components, +and manage devices using a Raspberry Pi on an event based system +using redis. +""" + import datetime +import json import socket -import redis +import threading import time -import json -import sys -sys.path.append('..') + +from adafruit_platformdetect import Detector + + +import redis + +import constants from action import Action -from config_load import loadConfigJson from server.mudpi_server import MudpiServer -from workers.pi.lcd_worker import LcdWorker -from workers.pi.i2c_worker import PiI2CWorker -from workers.pi.relay_worker import RelayWorker -from workers.pi.camera_worker import CameraWorker -from workers.pi.sensor_worker import PiSensorWorker -from workers.pi.control_worker import PiControlWorker -from workers.trigger_worker import TriggerWorker +from utils import load_config_json +from workers.linux.control_worker import PiControlWorker +from workers.linux.i2c_worker import PiI2CWorker +from workers.linux.lcd_worker import LcdWorker +from workers.linux.relay_worker import RelayWorker +from workers.linux.sensor_worker import PiSensorWorker from workers.sequence_worker import SequenceWorker +from workers.trigger_worker import TriggerWorker + try: - from workers.arduino.arduino_worker import ArduinoWorker - NANPY_ENABLED = True + from workers.arduino.arduino_worker import ArduinoWorker + + NANPY_ENABLED = True except ImportError: - NANPY_ENABLED = False + NANPY_ENABLED = False + try: - from workers.adc_worker import ADCMCP3008Worker - MCP_ENABLED = True -except ImportError: - MCP_ENABLED = False + from workers.adc_worker import ADCMCP3008Worker + + MCP_ENABLED = True +except (ImportError, AttributeError): + MCP_ENABLED = False from logger.Logger import Logger, LOG_LEVEL -import variables - -############################## -# MudPi Core -# Author: Eric Davisson (@theDavisson) [EricDavisson.com] -# https://mudpi.app -# MudPi Core is a python library to gather sensor readings, control components, -# and manage devices using a Raspberry Pi on an event based system using redis. -# -CONFIGS = {} + +detector = Detector() +if detector.board.any_raspberry_pi: + from workers.linux.camera_worker import CameraWorker + + PROGRAM_RUNNING = True threads = [] actions = {} @@ -54,24 +65,34 @@ print(chr(27) + "[2J") print('Loading MudPi Configs...\r', end="", flush=True) -CONFIGS = loadConfigJson() + +try: + CONFIGS = load_config_json() +except FileNotFoundError: + print( + f'{constants.RED_BACK}There is no configuration file present on the ' + f'filesystem{constants.RESET}\n\r' + ) + exit(1) + # Singleton redis to prevent connection conflicts try: - r = redis.Redis(host=CONFIGS['redis'].get('host', '127.0.0.1'), port=int(CONFIGS['redis'].get('port', 6379))) + r = redis.Redis(host=CONFIGS['redis'].get('host', '127.0.0.1'), + port=int(CONFIGS['redis'].get('port', 6379))) except KeyError: - r = redis.Redis(host='127.0.0.1', port=6379) + r = redis.Redis(host='127.0.0.1', port=6379) # Waiting for redis and services to be running -time.sleep(5) -print('Loading MudPi Configs...\t\033[1;32m Complete\033[0;0m') +time.sleep(5) +print('Loading MudPi Configs...\t\033[1;32m Complete\033[0;0m') print(chr(27) + "[2J") # Print a display logo for startup print("\033[1;32m") -print(' __ __ _ _____ _ ') -print('| \/ | | | __ (_)') -print('| \ / |_ _ __| | |__) | ') -print('| |\/| | | | |/ _` | ___/ | ') -print('| | | | |_| | (_| | | | | ') -print('|_| |_|\__,_|\__,_|_| |_| ') +print(r' __ __ _ _____ _ ') +print(r'| \/ | | | __ (_)') +print(r'| \ / |_ _ __| | |__) | ') +print(r'| |\/| | | | |/ _` | ___/ | ') +print(r'| | | | |_| | (_| | | | | ') +print(r'|_| |_|\__,_|\__,_|_| |_| ') print('_________________________________________________') print('') print('Eric Davisson @theDavisson') @@ -80,233 +101,390 @@ print('\033[0;0m') if CONFIGS['debug'] is True: - print('\033[1;33mDEBUG MODE ENABLED\033[0;0m') - print("Loaded Config\n--------------------") - for index, config in CONFIGS.items(): - if config != '': - print('%s: %s' % (index, config)) - time.sleep(10) + print('\033[1;33mDEBUG MODE ENABLED\033[0;0m') + print("Loaded Config\n--------------------") + for index, config in CONFIGS.items(): + if config != '': + print('%s: %s' % (index, config)) + time.sleep(10) try: - print('Initializing Logger \r', end="", flush=True) - Logger.logger = Logger(CONFIGS) - time.sleep(0.05) - Logger.log(LOG_LEVEL["info"], 'Initializing Logger...\t\t\t\033[1;32m Complete\033[0;0m') - Logger.log_to_file(LOG_LEVEL["debug"], "Dumping the config file: ") - for index, config in CONFIGS.items(): - Logger.log_to_file(LOG_LEVEL["debug"], '%s: %s' % (index, config)) - Logger.log_to_file(LOG_LEVEL["debug"], "End of config file dump!\n") + print('Initializing Logger \r', end="", flush=True) + Logger.logger = Logger(CONFIGS) + time.sleep(0.05) + Logger.log( + LOG_LEVEL["info"], + 'Initializing Logger...\t\t\t\033[1;32m Complete\033[0;0m' + ) + Logger.log_to_file(LOG_LEVEL["debug"], "Dumping the config file: ") + for index, config in CONFIGS.items(): + Logger.log_to_file(LOG_LEVEL["debug"], '%s: %s' % (index, config)) + Logger.log_to_file(LOG_LEVEL["debug"], "End of config file dump!\n") except Exception as e: - Logger.log(LOG_LEVEL["info"], 'Initializing Logger...\t\t\t\033[1;31m Disabled\033[0;0m') + Logger.log( + LOG_LEVEL["info"], + 'Initializing Logger...\t\t\t\033[1;31m Disabled\033[0;0m' + ) try: - print('Initializing Garden Control \r', end="", flush=True) - GPIO.setwarnings(False) - GPIO.setmode(GPIO.BCM) - GPIO.cleanup() - # Pause for GPIO to finish - time.sleep(0.1) - Logger.log(LOG_LEVEL["info"], 'Initializing Garden Control...\t\t\033[1;32m Complete\033[0;0m') - print('Preparing Threads for Workers\r', end="", flush=True) - - new_messages_waiting = threading.Event() # Event to signal LCD to pull new messages - main_thread_running = threading.Event() # Event to signal workers to close - system_ready = threading.Event() # Event to tell workers to begin working - camera_available = threading.Event() # Event to signal if camera can be used - lcd_available = threading.Event() # Event to signal if lcd displays can be used - - main_thread_running.set() # Main event to tell workers to run/shutdown - time.sleep(0.1) - Logger.log(LOG_LEVEL["info"], 'Preparing Threads for Workers...\t\033[1;32m Complete\033[0;0m') - - # Worker for Camera - try: - if len(CONFIGS["camera"]) > 0: - CONFIGS["camera"]["redis"] = r - c = CameraWorker(CONFIGS['camera'], main_thread_running, system_ready, camera_available) - Logger.log(LOG_LEVEL["info"], 'Camera...\t\t\t\033[1;32m Initializing\033[0;0m') - workers.append(c) - camera_available.set() - except KeyError: - Logger.log(LOG_LEVEL["info"], 'Pi Camera...\t\t\t\t\033[1;31m Disabled\033[0;0m') - - # Workers for pi (Sensors, Controls, Relays, I2C) - try: - if len(CONFIGS["workers"]) > 0: - for worker in CONFIGS['workers']: - # Create worker for worker - worker["redis"] = r - if worker['type'] == "sensor": - pw = PiSensorWorker(worker, main_thread_running, system_ready) - Logger.log(LOG_LEVEL["info"], 'Sensors...\t\t\t\t\033[1;32m Initializing\033[0;0m') - elif worker['type'] == "control": - pw = PiControlWorker(worker, main_thread_running, system_ready) - Logger.log(LOG_LEVEL["info"], 'Controls...\t\t\t\t\033[1;32m Initializing\033[0;0m') - elif worker['type'] == "i2c": - pw = PiI2CWorker(worker, main_thread_running, system_ready) - Logger.log(LOG_LEVEL["info"], 'I2C Comms...\t\t\t\t\033[1;32m Initializing\033[0;0m') - elif worker['type'] == "display": - for display in worker['displays']: - display["redis"] = r - pw = LcdWorker(display, main_thread_running, system_ready, lcd_available) - lcd_available.set() - Logger.log(LOG_LEVEL["info"], 'LCD Displays...\t\t\t\t\033[1;32m Initializing\033[0;0m') - elif worker['type'] == "relay": - # Add Relay Worker Here for Better Config Control - Logger.log(LOG_LEVEL["info"], 'Relay...\t\t\t\033[1;32m Initializing\033[0;0m') - else: - Logger.log(LOG_LEVEL["warning"], "Exception raised due to unknown Worker Type: {0}".format(worker['type'])) - raise Exception("Unknown Worker Type: " + worker['type']) - workers.append(pw) - except KeyError as e: - Logger.log(LOG_LEVEL["info"], 'Pi Workers...\t\t\t\t\033[1;31m Disabled\033[0;0m') - print(e) - - # Worker for relays attached to pi - try: - if len(CONFIGS["relays"]) > 0: - for relay in CONFIGS['relays']: - relay["redis"] = r - relayState = { - "available": threading.Event(), # Event to allow relay to activate - "active": threading.Event() # Event to signal relay to open/close - } - relayEvents[relay.get("key", relay_index)] = relayState - rw = RelayWorker(relay, main_thread_running, system_ready, relayState['available'], relayState['active']) - workers.append(rw) - # Make the relays available, this event is toggled off elsewhere if we need to disable relays - relayState['available'].set() - relay_index +=1 - except KeyError: - Logger.log(LOG_LEVEL["info"], 'Relays Workers...\t\t\033[1;31m Disabled\033[0;0m') - - # Load in Actions - try: - if len(CONFIGS["actions"]) > 0: - for action in CONFIGS["actions"]: - action["redis"] = r - a = Action(action) - a.init_action() - actions[a.key] = a - Logger.log(LOG_LEVEL["info"], '{0} Actions...\t\t\t\t\033[1;32m Initializing\033[0;0m'.format(len(CONFIGS['actions']))) - except KeyError: - Logger.log(LOG_LEVEL["info"], 'Actions...\t\t\t\033[1;31m Disabled\033[0;0m') - - # Worker for Sequences - try: - if len(CONFIGS["sequences"]) > 0: - for sequence in CONFIGS["sequences"]: - sequence["redis"] = r - sequenceState = { - "available": threading.Event(), # Event to allow sequence to activate - "active": threading.Event() # Event to signal sequence to open/close - } - sequenceEvents[sequence.get("key", sequence_index)] = sequenceState - s = SequenceWorker(sequence, main_thread_running, system_ready, sequenceState['available'], sequenceState['active'], actions) - workers.append(s) - sequences[s.key] = s - sequenceState['available'].set() - sequence_index +=1 - Logger.log(LOG_LEVEL["info"], '{0} Sequences...\t\t\t\t\033[1;32m Initializing\033[0;0m'.format(len(CONFIGS["sequences"]))) - except KeyError: - Logger.log(LOG_LEVEL["info"], 'Sequences...\t\t\t\t\033[1;31m Disabled\033[0;0m') - - # Worker for Triggers - try: - if len(CONFIGS["triggers"]) > 0: - for trigger in CONFIGS["triggers"]: - trigger["redis"] = r - t = TriggerWorker(CONFIGS['triggers'], main_thread_running, system_ready, actions, sequences) - Logger.log(LOG_LEVEL["info"], 'Triggers...\t\t\t\t\033[1;32m Initializing\033[0;0m') - workers.append(t) - except KeyError: - Logger.log(LOG_LEVEL["info"], 'Triggers...\t\t\t\t\033[1;31m Disabled\033[0;0m') - - - # Worker for nodes attached to pi via serial or wifi[esp8266, esp32] - # Supported nodes: arduinos, esp8266, ADC-MCP3xxx, probably others (esp32 with custom nanpy fork) - try: - if len(CONFIGS["nodes"]) > 0: - for node in CONFIGS['nodes']: - node["redis"] = r - if node['type'] == "arduino": - if NANPY_ENABLED: - Logger.log(LOG_LEVEL["info"], 'MudPi Arduino Workers...\t\t\033[1;32m Initializing\033[0;0m') - t = ArduinoWorker(node, main_thread_running, system_ready) - else: - Logger.log(LOG_LEVEL["error"], 'Error Loading Nanpy library. Did you pip3 install -r requirements.txt?') - elif node['type'] == "ADC-MCP3008": - if MCP_ENABLED: - Logger.log(LOG_LEVEL["info"], 'MudPi ADC Workers...\t\t\033[1;32m Initializing\033[0;0m') - t = ADCMCP3008Worker(node, main_thread_running, system_ready) - else: - Logger.log(LOG_LEVEL["error"], 'Error Loading MCP3xxx library. Did you pip3 install -r requirements.txt;?') - else: - Logger.log(LOG_LEVEL["warning"], "Exception raised due to unknown Node Type: {0}".format(node['type'])) - raise Exception("Unknown Node Type: " + node['type']) - nodes.append(t) - except KeyError as e: - Logger.log(LOG_LEVEL["info"], 'MudPi Node Workers...\t\t\t\033[1;31m Disabled\033[0;0m') - - # try: - # if (CONFIGS['server'] is not None): - # Logger.log(LOG_LEVEL["info"], 'MudPi Server...\t\t\t\t\033[1;33m Starting\033[0;0m', end='\r', flush=True) - # time.sleep(1) - # server = MudpiServer(main_thread_running, CONFIGS['server']['host'], CONFIGS['server']['port']) - # s = threading.Thread(target=server_worker) # TODO where is server_worker supposed to be initialized? - # threads.append(s) - # s.start() - # except KeyError: - # Logger.log(LOG_LEVEL["info"], 'MudPi Socket Server...\t\t\t\033[1;31m Disabled\033[0;0m') - - Logger.log(LOG_LEVEL["info"], 'MudPi Garden Controls...\t\t\033[1;32m Initialized\033[0;0m') - Logger.log(LOG_LEVEL["info"], 'Engaging MudPi Workers...\t\t\033[1;32m \033[0;0m') - - for worker in workers: - t = worker.run() - threads.append(t) - time.sleep(.5) - for node in nodes: - t = node.run() - threads.append(t) - time.sleep(.5) - - time.sleep(.5) - - Logger.log(LOG_LEVEL["info"], 'MudPi Garden Control...\t\t\t\033[1;32m Online\033[0;0m') - Logger.log(LOG_LEVEL["info"], '_________________________________________________') - system_ready.set() # Workers will not process until system is ready - r.set('started_at', str(datetime.datetime.now())) # Store current time to track uptime - system_message = {'event':'SystemStarted', 'data':1} - r.publish('mudpi', json.dumps(system_message)) - - # Hold the program here until its time to graceful shutdown - while PROGRAM_RUNNING: - # Main program loop - # add logging or other system operations here... - time.sleep(0.1) + print('Initializing Garden Control \r', end="", flush=True) + time.sleep(0.1) + Logger.log( + LOG_LEVEL["info"], + 'Initializing Garden Control...\t\t\033[1;32m Complete\033[0;0m' + ) + print('Preparing Threads for Workers\r', end="", flush=True) + + # Event to signal LCD to pull new messages + new_messages_waiting = threading.Event() + # Event to signal workers to close + main_thread_running = threading.Event() + # Event to tell workers to begin working + system_ready = threading.Event() + # Event to signal if camera can be used + camera_available = threading.Event() + # Event to signal if lcd displays can be used + lcd_available = threading.Event() + + # Main event to tell workers to run/shutdown + main_thread_running.set() + time.sleep(0.1) + Logger.log( + LOG_LEVEL["info"], + 'Preparing Threads for Workers...\t\033[1;32m Complete\033[0;0m' + ) + + # Worker for Camera + try: + if len(CONFIGS["camera"]) > 0: + CONFIGS["camera"]["redis"] = r + c = CameraWorker( + CONFIGS['camera'], + main_thread_running, + system_ready, + camera_available + ) + Logger.log( + LOG_LEVEL["info"], + 'Camera...\t\t\t\033[1;32m Initializing\033[0;0m' + ) + workers.append(c) + camera_available.set() + except KeyError: + Logger.log( + LOG_LEVEL["info"], + 'Pi Camera...\t\t\t\t\033[1;31m Disabled\033[0;0m' + ) + + # Workers for board (Sensors, Controls, Relays, I2C) + try: + if len(CONFIGS["workers"]) > 0: + + for worker in CONFIGS['workers']: + # Create worker for worker + worker["redis"] = r + + if worker['type'] == "sensor": + pw = PiSensorWorker( + worker, + main_thread_running, + system_ready + ) + Logger.log( + LOG_LEVEL["info"], + 'Sensors...\t\t\t\t\033[1;32m Initializing\033[0;0m' + ) + + elif worker['type'] == "control": + pw = PiControlWorker( + worker, + main_thread_running, + system_ready + ) + Logger.log( + LOG_LEVEL["info"], + 'Controls...\t\t\t\t\033[1;32m Initializing\033[0;0m' + ) + + elif worker['type'] == "i2c": + pw = PiI2CWorker(worker, main_thread_running, system_ready) + Logger.log( + LOG_LEVEL["info"], + 'I2C Comms...\t\t\t\t\033[1;32m Initializing\033[0;0m' + ) + + elif worker['type'] == "display": + for display in worker['displays']: + display["redis"] = r + pw = LcdWorker( + display, + main_thread_running, + system_ready, + lcd_available + ) + lcd_available.set() + Logger.log( + LOG_LEVEL["info"], + 'LCD Displays...\t\t\t\t\033[1;32m Initializing\033[0;0m' + ) + + elif worker['type'] == "relay": + # Add Relay Worker Here for Better Config Control + Logger.log(LOG_LEVEL["info"], + 'Relay...\t\t\t\033[1;32m Initializing\033[0;0m') + + else: + Logger.log( + LOG_LEVEL["warning"], + "Exception raised due to unknown Worker Type: {0}".format( + worker['type'])) + raise Exception("Unknown Worker Type: " + worker['type']) + workers.append(pw) + + except KeyError as e: + Logger.log( + LOG_LEVEL["info"], + 'Pi Workers...\t\t\t\t\033[1;31m Disabled\033[0;0m' + ) + print(e) + + # Worker for relays attached to board + try: + if len(CONFIGS["relays"]) > 0: + for relay in CONFIGS['relays']: + relay["redis"] = r + + relayState = { + # Event to allow relay to activate + "available": threading.Event(), + # Event to signal relay to open/close + "active": threading.Event() + } + relayEvents[relay.get("key", relay_index)] = relayState + rw = RelayWorker( + relay, + main_thread_running, + system_ready, + relayState['available'], + relayState['active'] + ) + workers.append(rw) + # Make the relays available, this event is toggled off + # elsewhere if we need to disable relays + relayState['available'].set() + relay_index += 1 + except KeyError: + Logger.log( + LOG_LEVEL["info"], + 'Relays Workers...\t\t\033[1;31m Disabled\033[0;0m' + ) + + # Load in Actions + try: + if len(CONFIGS["actions"]) > 0: + for action in CONFIGS["actions"]: + action["redis"] = r + a = Action(action) + a.init_action() + actions[a.key] = a + Logger.log( + LOG_LEVEL["info"], + '{0} Actions...\t\t\t\t\033[1;32m Initializing\033[0;0m'.format( + len(CONFIGS['actions'])) + ) + except KeyError: + Logger.log( + LOG_LEVEL["info"], + 'Actions...\t\t\t\t\033[1;31m Disabled\033[0;0m' + ) + + # Worker for Sequences + try: + if len(CONFIGS["sequences"]) > 0: + for sequence in CONFIGS["sequences"]: + sequence["redis"] = r + sequenceState = { + "available": threading.Event(), + # Event to allow sequence to activate + "active": threading.Event() + # Event to signal sequence to open/close + } + sequenceEvents[ + sequence.get("key", sequence_index)] = sequenceState + s = SequenceWorker( + sequence, + main_thread_running, + system_ready, + sequenceState['available'], + sequenceState['active'], + actions + ) + workers.append(s) + sequences[s.key] = s + sequenceState['available'].set() + sequence_index += 1 + Logger.log( + LOG_LEVEL["info"], + '{0} Sequences...\t\t\t\t\033[1;32m Initializing\033[0;0m'.format( + len(CONFIGS["sequences"])) + ) + except KeyError: + Logger.log( + LOG_LEVEL["info"], + 'Sequences...\t\t\t\t\033[1;31m Disabled\033[0;0m' + ) + + # Worker for Triggers + try: + if len(CONFIGS["triggers"]) > 0: + for trigger in CONFIGS["triggers"]: + trigger["redis"] = r + t = TriggerWorker( + CONFIGS['triggers'], + main_thread_running, + system_ready, + actions, + sequences + ) + Logger.log( + LOG_LEVEL["info"], + 'Triggers...\t\t\t\t\033[1;32m Initializing\033[0;0m' + ) + workers.append(t) + except KeyError: + Logger.log( + LOG_LEVEL["info"], + 'Triggers...\t\t\t\t\033[1;31m Disabled\033[0;0m' + ) + + # Worker for nodes attached to board via serial or wifi[esp8266, esp32] + # Supported nodes: arduinos, esp8266, ADC-mcp3xxx, probably others + # (esp32 with custom nanpy fork) + try: + if len(CONFIGS["nodes"]) > 0: + for node in CONFIGS['nodes']: + node["redis"] = r + + if node['type'] == "arduino": + if NANPY_ENABLED: + Logger.log( + LOG_LEVEL["info"], + 'MudPi Arduino Workers...\t\t\033[1;32m Initializing\033[0;0m' + ) + t = ArduinoWorker(node, main_thread_running, + system_ready) + else: + Logger.log( + LOG_LEVEL["error"], + 'Error Loading Nanpy library. Did you pip3 install -r requirements.txt?' + ) + + elif node['type'] == "ADC-MCP3008": + if MCP_ENABLED: + Logger.log( + LOG_LEVEL["info"], + 'MudPi ADC Workers...\t\t\033[1;32m Initializing\033[0;0m' + ) + t = ADCMCP3008Worker(node, main_thread_running, + system_ready) + else: + Logger.log( + LOG_LEVEL["error"], + 'Error Loading mcp3xxx library. Did you pip3 install -r requirements.txt;?' + ) + + else: + Logger.log( + LOG_LEVEL["warning"], + "Exception raised due to unknown Node Type: {0}".format( + node['type']) + ) + raise Exception("Unknown Node Type: " + node['type']) + nodes.append(t) + except KeyError as e: + Logger.log( + LOG_LEVEL["info"], + 'MudPi Node Workers...\t\t\t\033[1;31m Disabled\033[0;0m' + ) + + # try: + # if (CONFIGS['server'] is not None): + # Logger.log(LOG_LEVEL["info"], 'MudPi Server...\t\t\t\t\033[1;33m Starting\033[0;0m', end='\r', flush=True) + # time.sleep(1) + # server = MudpiServer(main_thread_running, CONFIGS['server']['host'], CONFIGS['server']['port']) + # s = threading.Thread(target=server_worker) # TODO where is server_worker supposed to be initialized? + # threads.append(s) + # s.start() + # except KeyError: + # Logger.log(LOG_LEVEL["info"], 'MudPi Socket Server...\t\t\t\033[1;31m Disabled\033[0;0m') + + Logger.log( + LOG_LEVEL["info"], + 'MudPi Garden Controls...\t\t\033[1;32m Initialized\033[0;0m' + ) + Logger.log( + LOG_LEVEL["info"], + 'Engaging MudPi Workers...\t\t\033[1;32m \033[0;0m' + ) + + for worker in workers: + t = worker.run() + threads.append(t) + time.sleep(.5) + for node in nodes: + t = node.run() + threads.append(t) + time.sleep(.5) + + time.sleep(.5) + + Logger.log( + LOG_LEVEL["info"], + 'MudPi Garden Control...\t\t\t\033[1;32m Online\033[0;0m' + ) + Logger.log( + LOG_LEVEL["info"], + '_________________________________________________' + ) + # Workers will not process until system is ready + system_ready.set() + # Store current time to track uptime + r.set('started_at', str(datetime.datetime.now())) + system_message = {'event': 'SystemStarted', 'data': 1} + r.publish('mudpi', json.dumps(system_message)) + + # Hold the program here until its time to graceful shutdown + while PROGRAM_RUNNING: + # Main program loop + # add logging or other system operations here... + time.sleep(0.1) except KeyboardInterrupt: - PROGRAM_RUNNING = False + PROGRAM_RUNNING = False finally: - Logger.log(LOG_LEVEL["info"], 'MudPi Shutting Down...') - # Perform any cleanup tasks here... - - try: - server.sock.shutdown(socket.SHUT_RDWR) - except: - pass - - # Clear main running event to signal threads to close - main_thread_running.clear() - - # Shutdown the camera loop - camera_available.clear() - - # Join all our threads for shutdown - for thread in threads: - thread.join() - - Logger.log(LOG_LEVEL["info"], "MudPi Shutting Down...\t\t\t\033[1;32m Complete\033[0;0m") - Logger.log(LOG_LEVEL["info"], "Mudpi is Now...\t\t\t\t\033[1;31m Offline\033[0;0m") - + Logger.log(LOG_LEVEL["info"], 'MudPi Shutting Down...') + # Perform any cleanup tasks here... + + try: + server.sock.shutdown(socket.SHUT_RDWR) + except Exception: + pass + + # Clear main running event to signal threads to close + main_thread_running.clear() + + # Shutdown the camera loop + camera_available.clear() + + # Join all our threads for shutdown + for thread in threads: + thread.join() + + Logger.log( + LOG_LEVEL["info"], + "MudPi Shutting Down...\t\t\t\033[1;32m Complete\033[0;0m" + ) + Logger.log( + LOG_LEVEL["info"], + "Mudpi is Now...\t\t\t\t\033[1;31m Offline\033[0;0m" + ) diff --git a/requirements.txt b/requirements.txt index bdaeea1..a150cb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ pycron redis picamera -Adafruit_DHT +Adafruit-Blinka +adafruit-circuitpython-dht adafruit-circuitpython-mcp3xxx -adafruit-blinka adafruit-circuitpython-bme680 +smbus https://github.com/olixr/nanpy/archive/master.zip -git+https://github.com/olixr/Adafruit_CircuitPython_CharLCD.git#egg=adafruit-circuitpython-charlcd \ No newline at end of file +git+https://github.com/olixr/Adafruit_CircuitPython_CharLCD.git#egg=adafruit-circuitpython-charlcd diff --git a/sensors/MCP3xxx/sensor.py b/sensors/MCP3xxx/sensor.py deleted file mode 100644 index de4ba77..0000000 --- a/sensors/MCP3xxx/sensor.py +++ /dev/null @@ -1,62 +0,0 @@ -import adafruit_mcp3xxx.mcp3008 as MCP -import redis - - -# Base sensor class to extend all other mcp3xxx sensors from. -class Sensor: - - PINS = { - 0: MCP.P0, - 1: MCP.P1, - 2: MCP.P2, - 3: MCP.P3, - 4: MCP.P4, - 5: MCP.P5, - 6: MCP.P6, - 7: MCP.P7, - } - - def __init__(self, pin: int, mcp, name=None, key=None, redis_conn=None): - self.pin = pin - self.mcp = mcp - self.topic = None - - if key is None: - raise Exception('No "key" Found in Sensor Config') - else: - self.key = key.replace(" ", "_").lower() - - if name is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = name - - try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - - def init_sensor(self): - """ - Initialize the sensor here (i.e. set pin mode, get addresses, etc) - :return: - """ - raise NotImplementedError - - def read(self): - """ - Read the sensor(s), parse the data and store it in redis if redis is configured - :return: - """ - raise NotImplementedError - - def readRaw(self): - """ - Read the sensor(s) but return the raw voltage, useful for debugging - :return: - """ - return self.topic.voltage - - def readPin(self): - """ Read the pin from the MCP3xxx as unaltered digital value""" - return self.topic.value diff --git a/sensors/arduino/float_sensor.py b/sensors/arduino/float_sensor.py index 86b15bb..5efa9e6 100644 --- a/sensors/arduino/float_sensor.py +++ b/sensors/arduino/float_sensor.py @@ -1,31 +1,25 @@ -import time -import datetime -import json -import redis -from .sensor import Sensor -from nanpy import (ArduinoApi, SerialManager) -import sys -sys.path.append('..') +from nanpy import (SerialManager) -import variables +from sensors.arduino.sensor import Sensor default_connection = SerialManager(device='/dev/ttyUSB0') -#r = redis.Redis(host='127.0.0.1', port=6379) + class FloatSensor(Sensor): - def __init__(self, pin, name=None, key=None, connection=default_connection, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, redis_conn=redis_conn) - return + def __init__(self, pin, name=None, key=None, connection=default_connection, + redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, + redis_conn=redis_conn) - def init_sensor(self): - # read data using pin specified pin - self.api.pinMode(self.pin, self.api.INPUT) + def init_sensor(self): + # read data using pin specified pin + self.api.pinMode(self.pin, self.api.INPUT) - def read(self): - value = self.api.digitalRead(self.pin) - self.r.set(self.key, value) - return value + def read(self): + value = self.api.digitalRead(self.pin) + self.r.set(self.key, value) + return value - def readRaw(self): - return self.read() \ No newline at end of file + def read_raw(self): + return self.read() diff --git a/sensors/arduino/humidity_sensor.py b/sensors/arduino/humidity_sensor.py index 1126e6b..0d74b88 100644 --- a/sensors/arduino/humidity_sensor.py +++ b/sensors/arduino/humidity_sensor.py @@ -5,42 +5,48 @@ from .sensor import Sensor from nanpy import (ArduinoApi, SerialManager, DHT) import sys -sys.path.append('..') -import variables + + +import constants default_connection = SerialManager(device='/dev/ttyUSB0') -#r = redis.Redis(host='127.0.0.1', port=6379) + + +# r = redis.Redis(host='127.0.0.1', port=6379) class HumiditySensor(Sensor): - def __init__(self, pin, name=None, key=None, connection=default_connection, model='11', api=None, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, redis_conn=redis_conn) - self.type = model #DHT11 or DHT22 maybe AM2302 - return - - def init_sensor(self): - # prepare sensor on specified pin - sensor_types = { '11': DHT.DHT11, - '22': DHT.DHT22, - '2301': DHT.AM2301 } - if self.type in sensor_types: - self.sensor = sensor_types[self.type] - else: - # print('Sensor Type Error: Defaulting to DHT11') - self.sensor = DHT.DHT11 - self.dht = DHT(self.pin, self.sensor, connection=self.connection) - - def read(self): - #Pass true to read in american degrees :) - temperature = self.dht.readTemperature(True) - humidity = self.dht.readHumidity() - data = {'temperature': round(temperature, 2), 'humidity': round(humidity, 2)} - self.r.set(self.key + '_temperature', temperature) - self.r.set(self.key + '_humidity', humidity) - self.r.set(self.key, json.dumps(data)) - return data - - def readRaw(self): - return self.read() \ No newline at end of file + def __init__(self, pin, name=None, key=None, connection=default_connection, + model='11', api=None, redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, + redis_conn=redis_conn) + self.type = model # DHT11 or DHT22 maybe AM2302 + return + + def init_sensor(self): + # prepare sensor on specified pin + sensor_types = {'11': DHT.DHT11, + '22': DHT.DHT22, + '2301': DHT.AM2301} + if self.type in sensor_types: + self.sensor = sensor_types[self.type] + else: + # print('Sensor Type Error: Defaulting to DHT11') + self.sensor = DHT.DHT11 + self.dht = DHT(self.pin, self.sensor, connection=self.connection) + + def read(self): + # Pass true to read in american degrees :) + temperature = self.dht.readTemperature(True) + humidity = self.dht.readHumidity() + data = {'temperature': round(temperature, 2), + 'humidity': round(humidity, 2)} + self.r.set(self.key + '_temperature', temperature) + self.r.set(self.key + '_humidity', humidity) + self.r.set(self.key, json.dumps(data)) + return data + + def read_raw(self): + return self.read() diff --git a/sensors/arduino/light_sensor.py b/sensors/arduino/light_sensor.py index deae097..90f35c8 100644 --- a/sensors/arduino/light_sensor.py +++ b/sensors/arduino/light_sensor.py @@ -5,27 +5,32 @@ from .sensor import Sensor from nanpy import (ArduinoApi, SerialManager) import sys -sys.path.append('..') -import variables + + +import constants default_connection = SerialManager(device='/dev/ttyUSB0') -#r = redis.Redis(host='127.0.0.1', port=6379) + + +# r = redis.Redis(host='127.0.0.1', port=6379) class LightSensor(Sensor): - def __init__(self, pin, name=None, key=None, connection=default_connection, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, redis_conn=redis_conn) - return + def __init__(self, pin, name=None, key=None, connection=default_connection, + redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, + redis_conn=redis_conn) + return - def init_sensor(self): - # read data using pin specified pin - self.api.pinMode(self.pin, self.api.INPUT) + def init_sensor(self): + # read data using pin specified pin + self.api.pinMode(self.pin, self.api.INPUT) - def read(self): - light_intesity = self.api.analogRead(self.pin) - self.r.set(self.key, light_intesity) - return light_intesity + def read(self): + light_intesity = self.api.analogRead(self.pin) + self.r.set(self.key, light_intesity) + return light_intesity - def readRaw(self): - return self.read() \ No newline at end of file + def read_raw(self): + return self.read() diff --git a/sensors/arduino/rain_sensor.py b/sensors/arduino/rain_sensor.py index dbdb3eb..984b88e 100644 --- a/sensors/arduino/rain_sensor.py +++ b/sensors/arduino/rain_sensor.py @@ -5,68 +5,74 @@ from .sensor import Sensor from nanpy import (ArduinoApi, SerialManager) import sys -sys.path.append('..') -import variables + + +import constants + default_connection = SerialManager(device='/dev/ttyUSB0') -#r = redis.Redis(host='127.0.0.1', port=6379) +# r = redis.Redis(host='127.0.0.1', port=6379) -#The resistor reads lower the more water on the sensor -NO_RAIN_BOUNDS = 1000 # and above +# The resistor reads lower the more water on the sensor +NO_RAIN_BOUNDS = 1000 # and above MIST_BOUNDS = 800 LIGHT_RAIN_BOUNDS = 750 RAIN_BOUNDS = 550 HEAVY_RAIN_BOUNDS = 400 -DOWNPOUR_BOUNDS = 300 # and below +DOWNPOUR_BOUNDS = 300 # and below + class RainSensor(Sensor): - def __init__(self, pin, name=None, key=None, connection=default_connection, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, redis_conn=redis_conn) - return + def __init__(self, pin, name=None, key=None, connection=default_connection, + redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, + redis_conn=redis_conn) + return + + def init_sensor(self): + # read data using pin specified pin + self.api.pinMode(self.pin, self.api.INPUT) - def init_sensor(self): - # read data using pin specified pin - self.api.pinMode(self.pin, self.api.INPUT) + def read(self): + rain = self.api.analogRead(self.pin) # TODO: REMOVE (PERSONAL CONFIG) + # rain = self.parseSensorReading(self.api.analogRead(self.pin)) + self.r.set(self.key, rain) + return rain - def read(self): - rain = self.api.analogRead(self.pin) #TODO: REMOVE (PERSONAL CONFIG) - #rain = self.parseSensorReading(self.api.analogRead(self.pin)) - self.r.set(self.key, rain) - return rain + def read_raw(self): + resistance = self.api.analogRead(self.pin) + # print("Resistance: %d" % resistance) + self.r.set(self.key + '_raw', resistance) + return resistance - def readRaw(self): - resistance = self.api.analogRead(self.pin) - #print("Resistance: %d" % resistance) - self.r.set(self.key+'_raw', resistance) - return resistance + def parseSensorReading(self, raw_data): + if (raw_data > MIST_BOUNDS): + return 'No Rain' + elif (raw_data <= MIST_BOUNDS and raw_data > LIGHT_RAIN_BOUNDS): + return 'Mist' + elif (raw_data <= LIGHT_RAIN_BOUNDS and raw_data > RAIN_BOUNDS): + return 'Light Rain' + elif (raw_data <= RAIN_BOUNDS and raw_data > HEAVY_RAIN_BOUNDS): + return 'Rain' + elif (raw_data <= HEAVY_RAIN_BOUNDS and raw_data > DOWNPOUR_BOUNDS): + return 'Heavy Rain' + elif (raw_data <= DOWNPOUR_BOUNDS): + return 'Downpour' + else: + return 'Bad Sensor Data' - def parseSensorReading(self, raw_data): - if(raw_data > MIST_BOUNDS): - return 'No Rain' - elif(raw_data <= MIST_BOUNDS and raw_data > LIGHT_RAIN_BOUNDS): - return 'Mist' - elif(raw_data <= LIGHT_RAIN_BOUNDS and raw_data > RAIN_BOUNDS): - return 'Light Rain' - elif(raw_data <= RAIN_BOUNDS and raw_data > HEAVY_RAIN_BOUNDS): - return 'Rain' - elif(raw_data <= HEAVY_RAIN_BOUNDS and raw_data > DOWNPOUR_BOUNDS): - return 'Heavy Rain' - elif(raw_data <= DOWNPOUR_BOUNDS): - return 'Downpour' - else: - return 'Bad Sensor Data' if __name__ == '__main__': - try: - loop_count = 10 - while (loop_count > 0): - sensor = RainSensor(4) - rainread = sensor.read() - print('Rain: ', rainread) - loop_count += 1 - time.sleep(1) - except KeyboardInterrupt: - pass - finally: - print('Rain Sensor Closing...') \ No newline at end of file + try: + loop_count = 10 + while (loop_count > 0): + sensor = RainSensor(4) + rainread = sensor.read() + print('Rain: ', rainread) + loop_count += 1 + time.sleep(1) + except KeyboardInterrupt: + pass + finally: + print('Rain Sensor Closing...') diff --git a/sensors/arduino/sensor.py b/sensors/arduino/sensor.py index 14244e3..60e61d4 100644 --- a/sensors/arduino/sensor.py +++ b/sensors/arduino/sensor.py @@ -2,45 +2,54 @@ import json import redis from nanpy import (ArduinoApi, SerialManager) +from sensors.base_sensor import BaseSensor default_connection = SerialManager() -# Base sensor class to extend all other arduino sensors from. -class Sensor(): - - def __init__(self, pin, name=None, connection=default_connection, analog_pin_mode=False, key=None, api=None, redis_conn=None): - self.pin = pin - if key is None: - raise Exception('No "key" Found in Sensor Config') - else: - self.key = key.replace(" ", "_").lower() - - if name is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = name - self.analog_pin_mode = analog_pin_mode - self.connection = connection - self.api = api if api is not None else ArduinoApi(connection) - try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - return - - def init_sensor(self): - #Initialize the sensor here (i.e. set pin mode, get addresses, etc) - pass - - def read(self): - #Read the sensor(s), parse the data and store it in redis if redis is configured - pass - - def readRaw(self): - #Read the sensor(s) but return the raw data, useful for debugging - pass - - def readPin(self): - #Read the pin from the ardiuno. Can be analog or digital based on "analog_pin_mode" - data = self.api.analogRead(self.pin) if analog_pin_mode else self.api.digitalRead(self.pin) - return data \ No newline at end of file + +class Sensor(BaseSensor): + """ + Base sensor class to extend all other arduino sensors from. + """ + + def __init__(self, pin, name=None, connection=default_connection, + analog_pin_mode=False, key=None, api=None, redis_conn=None): + """ + + Args: + pin: + name: + connection: + analog_pin_mode: + key: + api: + redis_conn: + """ + + super().__init__( + pin=pin, + name=name, + key=key, + redis_conn=redis_conn + ) + + self.analog_pin_mode = analog_pin_mode + self.connection = connection + self.api = api if api is not None else ArduinoApi(connection) + + def read_pin(self): + """ + Read the pin from the ardiuno. Can be analog or digital based on + "analog_pin_mode" + + Returns: + + """ + + if self.analog_pin_mode: + data = self.api.analogRead(self.pin) + + else: + data = self.api.digitalRead(self.pin) + + return data diff --git a/sensors/arduino/soil_sensor.py b/sensors/arduino/soil_sensor.py index 0f29e1a..414ef88 100644 --- a/sensors/arduino/soil_sensor.py +++ b/sensors/arduino/soil_sensor.py @@ -5,46 +5,53 @@ from .sensor import Sensor from nanpy import (ArduinoApi, SerialManager) import sys -sys.path.append('..') -import variables + + +import constants default_connection = SerialManager(device='/dev/ttyUSB0') -#r = redis.Redis(host='127.0.0.1', port=6379) +# r = redis.Redis(host='127.0.0.1', port=6379) # Wet Water = 287 # Dry Air = 584 AirBounds = 590; WaterBounds = 280; -intervals = int((AirBounds - WaterBounds)/3); +intervals = int((AirBounds - WaterBounds) / 3); + + class SoilSensor(Sensor): - def __init__(self, pin, name=None, key=None, connection=default_connection, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, redis_conn=redis_conn) - return - - def init_sensor(self): - # read data using pin specified pin - self.api.pinMode(self.pin, self.api.INPUT) - - def read(self): - resistance = self.api.analogRead(self.pin) - moistpercent = ((resistance - WaterBounds) / (AirBounds - WaterBounds)) * 100 - if(moistpercent > 80): - moisture = 'Very Dry - ' + str(int(moistpercent)) - elif(moistpercent <= 80 and moistpercent > 45): - moisture = 'Dry - ' + str(int(moistpercent)) - elif(moistpercent <= 45 and moistpercent > 25): - moisture = 'Wet - ' + str(int(moistpercent)) - else: - moisture = 'Very Wet - ' + str(int(moistpercent)) - #print("Resistance: %d" % resistance) - #TODO: Put redis store into sensor worker - self.r.set(self.key, resistance) #TODO: CHANGE BACK TO 'moistpercent' (PERSONAL CONFIG) - return resistance - - def readRaw(self): - resistance = self.api.analogRead(self.pin) - #print("Resistance: %d" % resistance) - self.r.set(self.key+'_raw', resistance) - return resistance \ No newline at end of file + def __init__(self, pin, name=None, key=None, connection=default_connection, + redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, + redis_conn=redis_conn) + return + + def init_sensor(self): + # read data using pin specified pin + self.api.pinMode(self.pin, self.api.INPUT) + + def read(self): + resistance = self.api.analogRead(self.pin) + moistpercent = ((resistance - WaterBounds) / ( + AirBounds - WaterBounds)) * 100 + if (moistpercent > 80): + moisture = 'Very Dry - ' + str(int(moistpercent)) + elif (moistpercent <= 80 and moistpercent > 45): + moisture = 'Dry - ' + str(int(moistpercent)) + elif (moistpercent <= 45 and moistpercent > 25): + moisture = 'Wet - ' + str(int(moistpercent)) + else: + moisture = 'Very Wet - ' + str(int(moistpercent)) + # print("Resistance: %d" % resistance) + # TODO: Put redis store into sensor worker + self.r.set(self.key, + resistance) # TODO: CHANGE BACK TO 'moistpercent' (PERSONAL CONFIG) + return resistance + + def read_raw(self): + resistance = self.api.analogRead(self.pin) + # print("Resistance: %d" % resistance) + self.r.set(self.key + '_raw', resistance) + return resistance diff --git a/sensors/arduino/temperature_sensor.py b/sensors/arduino/temperature_sensor.py index ff1c35e..077c40e 100644 --- a/sensors/arduino/temperature_sensor.py +++ b/sensors/arduino/temperature_sensor.py @@ -7,63 +7,70 @@ import sys from logger.Logger import Logger, LOG_LEVEL -sys.path.append('..') -import variables + + +import constants default_connection = SerialManager(device='/dev/ttyUSB0') -#r = redis.Redis(host='127.0.0.1', port=6379) + + +# r = redis.Redis(host='127.0.0.1', port=6379) class TemperatureSensor(Sensor): - def __init__(self, pin, name=None, key=None, connection=default_connection, redis_conn=None): - super().__init__(pin, name=name, key=key, connection=connection, redis_conn=redis_conn) - return - - def init_sensor(self): - self.sensors = DallasTemperature(self.pin, connection=self.connection) - self.sensor_bus = self.sensors.getDeviceCount() - # read data using pin specified pin - Logger.log(LOG_LEVEL["debug"], "There are",self.sensor_bus, "devices connected on pin ", self.sensors.pin) - self.addresses = [] - - for i in range(self.sensor_bus): - self.addresses.append(self.sensors.getAddress(i)) - - Logger.log(LOG_LEVEL["debug"], "Their addresses", self.addresses) - #I guess this is something with bit rates? TODO: Look this up - self.sensors.setResolution(10) - - #sensor = id of sensor you want in addresses[] - def read(self): - #temp = self.sensors.getTempF(sensor) - #self.r.set('temp_'+str(sensor), temp) - #return temp - return self.readAll() - - def readAll(self): - self.sensors.requestTemperatures() - temps = {} - for i in range(self.sensor_bus): - temp = self.sensors.getTempC(i) - temps['temp_'+str(i)] = temp - #self.r.set(self.key+'_'+str(i), temp) - #print("Device %d (%s) " % (i, self.addresses[i])) - #print("Let's convert it in Fahrenheit degrees: %0.2f" % DallasTemperature.toFahrenheit(temp)) - self.r.set(self.key, temps) - return temps + def __init__(self, pin, name=None, key=None, connection=default_connection, + redis_conn=None): + super().__init__(pin, name=name, key=key, connection=connection, + redis_conn=redis_conn) + return + + def init_sensor(self): + self.sensors = DallasTemperature(self.pin, connection=self.connection) + self.sensor_bus = self.sensors.getDeviceCount() + # read data using pin specified pin + Logger.log(LOG_LEVEL["debug"], "There are", self.sensor_bus, + "devices connected on pin ", self.sensors.pin) + self.addresses = [] + + for i in range(self.sensor_bus): + self.addresses.append(self.sensors.getAddress(i)) + + Logger.log(LOG_LEVEL["debug"], "Their addresses", self.addresses) + # I guess this is something with bit rates? TODO: Look this up + self.sensors.setResolution(10) + + # sensor = id of sensor you want in addresses[] + def read(self): + # temp = self.sensors.getTempF(sensor) + # self.r.set('temp_'+str(sensor), temp) + # return temp + return self.readAll() + + def readAll(self): + self.sensors.requestTemperatures() + temps = {} + for i in range(self.sensor_bus): + temp = self.sensors.getTempC(i) + temps['temp_' + str(i)] = temp + # self.r.set(self.key+'_'+str(i), temp) + # print("Device %d (%s) " % (i, self.addresses[i])) + # print("Let's convert it in Fahrenheit degrees: %0.2f" % DallasTemperature.toFahrenheit(temp)) + self.r.set(self.key, temps) + return temps + if __name__ == '__main__': - try: - loop_count = 10 - sensor = TemperatureSensor(2) - sensor.init_sensor() - while (loop_count > 0): - tempread = sensor.readAll() - print('Temps: ', tempread) - loop_count += 1 - time.sleep(2) - except KeyboardInterrupt: - pass - finally: - print('Temp Sensors Closing...') \ No newline at end of file + try: + loop_count = 10 + sensor = TemperatureSensor(2) + sensor.init_sensor() + while (loop_count > 0): + tempread = sensor.readAll() + print('Temps: ', tempread) + loop_count += 1 + time.sleep(2) + except KeyboardInterrupt: + pass + finally: + print('Temp Sensors Closing...') diff --git a/sensors/base_sensor.py b/sensors/base_sensor.py new file mode 100644 index 0000000..50d9cf5 --- /dev/null +++ b/sensors/base_sensor.py @@ -0,0 +1,35 @@ +import redis + + +class BaseSensor: + + def __init__(self, pin, name=None, key=None, redis_conn=None): + self.pin = pin + + if key is None: + raise Exception('No "key" Found in Sensor Config') + else: + self.key = key.replace(" ", "_").lower() + + if name is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = name + + try: + self.r = redis_conn if redis_conn is not None else redis.Redis( + host='127.0.0.1', port=6379) + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + + def init_sensor(self): + pass + + def read(self): + pass + + def read_raw(self): + pass + + def read_pin(self): + raise NotImplementedError diff --git a/sensors/MCP3xxx/__init__.py b/sensors/linux/__init__.py similarity index 100% rename from sensors/MCP3xxx/__init__.py rename to sensors/linux/__init__.py diff --git a/sensors/linux/float_sensor.py b/sensors/linux/float_sensor.py new file mode 100644 index 0000000..2323658 --- /dev/null +++ b/sensors/linux/float_sensor.py @@ -0,0 +1,32 @@ +import board +import digitalio + +from .sensor import Sensor + + +# PIN MODE : OUT | IN + +class FloatSensor(Sensor): + + def __init__(self, pin, name=None, key=None, redis_conn=None): + super().__init__(pin, name=name, key=key, redis_conn=redis_conn) + self.pin_obj = getattr(board, pin) + return + + def init_sensor(self): + """Initialize the sensor here (i.e. set pin mode, get addresses, etc) + this gets called by the worker""" + # Default to input : + # https://github.com/adafruit/Adafruit_Blinka/blob/master/src/digitalio.py#L111 + self.gpio_pin = digitalio.DigitalInOut(self.pin_obj) + return + + def read(self): + """Read the sensor(s), parse the data and store it in redis if redis + is configured""" + value = self.gpio_pin.value + return value + + def read_raw(self): + """Read the sensor(s) but return the raw data, useful for debugging""" + return self.read() diff --git a/sensors/linux/humidity_sensor.py b/sensors/linux/humidity_sensor.py new file mode 100644 index 0000000..e8fa796 --- /dev/null +++ b/sensors/linux/humidity_sensor.py @@ -0,0 +1,100 @@ +import json +import sys +import time +import board +import adafruit_dht + +from sensors.linux.sensor import Sensor + + + +from logger.Logger import Logger, LOG_LEVEL + + +# PIN MODE : OUT | IN + + +class HumiditySensor(Sensor): + + def __init__(self, pin, name=None, key=None, model='11', redis_conn=None): + super().__init__(pin, name=name, key=key, redis_conn=redis_conn) + self.pin_obj = getattr(board, pin) + self.type = model + return + + def init_sensor(self): + """ + Initialize the sensor here (i.e. set pin mode, get addresses, etc) + this gets called by the worker + """ + sensor_types = { + '11': adafruit_dht.DHT11, + '22': adafruit_dht.DHT22, + '2302': adafruit_dht.DHT22 + } # AM2302 = DHT22 + if self.type in sensor_types: + self.sensor = sensor_types[self.type] + else: + Logger.log( + LOG_LEVEL["warning"], + 'Sensor Model Error: Defaulting to DHT11' + ) + self.sensor = adafruit_dht.DHT11 + return + + def read(self): + """ + Read the sensor(s), parse the data and store it in redis if redis + is configured + """ + # Set values just in case we never set them up. + + humidity = None + temperature_c = None + + # read_retry() not implemented in new lib + for i in range(15): + dhtDevice = self.sensor(self.pin_obj) + + try: + temperature_c = dhtDevice.temperature + humidity = dhtDevice.humidity + if humidity is not None and temperature_c is not None: + dhtDevice.exit() + break + + except RuntimeError: + # Errors happen fairly often, DHT's are hard to read, + # just keep going: + time.sleep(2) + continue + + if humidity is not None and temperature_c is not None: + self.r.set( + self.key + '_temperature', + round(temperature_c * 1.8 + 32, 2) + ) + self.r.set( + self.key + '_humidity', humidity + ) + readings = { + 'temperature': round(temperature_c * 1.8 + 32, 2), + 'humidity': round(humidity, 2) + } + self.r.set(self.key, json.dumps(readings)) + return readings + else: + Logger.log( + LOG_LEVEL["error"], + 'DHT Reading was Invalid. Trying again next cycle.' + ) + return None + + def read_raw(self): + """ + Read the sensor(s) but return the raw data, useful for debugging + + Returns: + + """ + return self.read() diff --git a/sensors/pi/__init__.py b/sensors/linux/i2c/__init__.py similarity index 100% rename from sensors/pi/__init__.py rename to sensors/linux/i2c/__init__.py diff --git a/sensors/linux/i2c/bme680_sensor.py b/sensors/linux/i2c/bme680_sensor.py new file mode 100644 index 0000000..649762b --- /dev/null +++ b/sensors/linux/i2c/bme680_sensor.py @@ -0,0 +1,57 @@ +import json +import sys + +import adafruit_bme680 + +from logger.Logger import Logger, LOG_LEVEL +from sensors.linux.i2c.sensor import Sensor + + + + +class Bme680Sensor(Sensor): + + def __init__(self, address=None, name=None, key=None, redis_conn=None): + super().__init__(address, name=name, key=key, redis_conn=redis_conn) + return + + def init_sensor(self): + self.sensor = adafruit_bme680.Adafruit_BME680_I2C( + self.i2c, debug=False + ) + # change this to match the location's pressure (hPa) at sea level + self.sensor.sea_level_pressure = 1013.25 + return + + def read(self): + temperature = round((self.sensor.temperature - 5) * 1.8 + 32, 2) + gas = self.sensor.gas + humidity = round(self.sensor.humidity, 1) + pressure = round(self.sensor.pressure, 2) + altitude = round(self.sensor.altitude, 3) + + if humidity is not None and temperature is not None: + self.r.set(self.key + '_temperature', temperature) + self.r.set(self.key + '_humidity', humidity) + self.r.set(self.key + '_gas', gas) + self.r.set(self.key + '_pressure', pressure) + self.r.set(self.key + '_altitude', altitude) + readings = { + 'temperature': temperature, + 'humidity': humidity, + 'pressure': pressure, + 'gas': gas, + 'altitude': altitude + } + self.r.set(self.key, json.dumps(readings)) + # print('BME680:', readings) + return readings + else: + Logger.log( + LOG_LEVEL["error"], + 'Failed to get reading [BME680]. Try again!' + ) + + def read_raw(self): + """Read the sensor(s) but return the raw data, useful for debugging""" + return self.read() diff --git a/sensors/linux/i2c/sensor.py b/sensors/linux/i2c/sensor.py new file mode 100644 index 0000000..60d060e --- /dev/null +++ b/sensors/linux/i2c/sensor.py @@ -0,0 +1,53 @@ +import time +import json +import redis +import board +from busio import I2C + + +# PIN MODE : OUT | IN + +class Sensor(): + + def __init__(self, address, name=None, key=None, redis_conn=None): + self.address = address + + if key is None: + raise Exception('No "key" Found in I2C Sensor Config') + else: + self.key = key.replace(" ", "_").lower() + + if name is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = name + + self.i2c = I2C(board.SCL, board.SDA) + try: + self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + return + + def init_sensor(self): + """Initialize the sensor here (i.e. set pin mode, get addresses, etc)""" + # GPIO.setmode(GPIO.BCM) + # GPIO.setup(pin, GPIO.IN) + pass + + def read(self): + """Read the sensor(s), parse the data and store it in redis if redis is configured""" + # GPIO.input(pin) + pass + + def read_raw(self): + """Read the sensor(s) but return the raw data, useful for debugging""" + pass + + # self.pin not defined and read_pin() doesn't seemto be called. So I commented it + ''' + def read_pin(self): + """Read the pin from the board. Can be analog or digital based on \"analog_pin_mode\"""" + data = self.gpio.input(self.pin) + return data + ''' diff --git a/sensors/linux/i2c/t9602_sensor.py b/sensors/linux/i2c/t9602_sensor.py new file mode 100644 index 0000000..5eab007 --- /dev/null +++ b/sensors/linux/i2c/t9602_sensor.py @@ -0,0 +1,53 @@ +import json +import sys + +import smbus + +from logger.Logger import Logger, LOG_LEVEL +from sensors.linux.i2c.sensor import Sensor + + + + +class T9602Sensor(Sensor): + + def __init__(self, address=None, name=None, key=None, redis_conn=None): + super().__init__(address, name=name, key=key, redis_conn=redis_conn) + self.address = address + return + + def init_sensor(self): + '''This is the bus number : the 1 in "/dev/i2c-1" + I enforced it to 1 because there is only one on Raspberry Pi. + We might want to add this parameter in i2c sensor config in the future. + We might encounter boards with several buses.''' + self.bus = smbus.SMBus(1) + return + + def read(self): + data = self.bus.read_i2c_block_data(self.address, 0, 4) + + humidity = (((data[0] & 0x3F) << 8) + data[1]) / 16384.0 * 100.0 + temperature_c = ((data[2] * 64) + (data[3] >> 2)) / 16384.0 * 165.0 - 40.0 + + humidity = round(humidity, 2) + temperature_c = round(temperature_c, 2) + + if humidity is not None and temperature_c is not None: + self.r.set(self.key + '_temperature', temperature_c) + self.r.set(self.key + '_humidity', humidity) + readings = { + 'temperature': temperature_c, + 'humidity': humidity + } + self.r.set(self.key, json.dumps(readings)) + return readings + else: + Logger.log( + LOG_LEVEL["error"], + 'Failed to get reading [t9602]. Try again!' + ) + + def read_raw(self): + """Read the sensor(s) but return the raw data, useful for debugging""" + return self.read() diff --git a/sensors/linux/sensor.py b/sensors/linux/sensor.py new file mode 100644 index 0000000..0a49947 --- /dev/null +++ b/sensors/linux/sensor.py @@ -0,0 +1,57 @@ +import re + +import board +import digitalio + +from logger.Logger import Logger, LOG_LEVEL +from sensors.base_sensor import BaseSensor + + +# PIN MODE : OUT | IN + +class Sensor(BaseSensor): + + def __init__(self, pin, name=None, key=None, redis_conn=None): + + super().__init__( + pin=pin, + name=name, + key=key, + redis_conn=redis_conn + ) + self.pin_obj = getattr(board, pin) + + if re.match(r'D\d+$', pin): + self.is_digital = True + elif re.match(r'A\d+$', pin): + self.is_digital = False + else: + Logger.log( + LOG_LEVEL["error"], + "Cannot detect pin type (Digital or analog), " + "should be Dxx or Axx for digital or analog. " + "Please refer to " + "https://github.com/adafruit/Adafruit_Blinka/tree/master/src/adafruit_blinka/board" + ) + + self.gpio = digitalio + + def read_pin(self): + """Read the pin from the board. + + Pin value must be a blinka Pin. + D for a digital input and A for an analog input, followed by the + pin number. + + You check the board-specific pin mapping + [here](https://github.com/adafruit/Adafruit_Blinka/blob/master/src/adafruit_blinka/board/). + + Examples: + read_pin(board.D12) + read_pin(board.A12) + """ + if self.is_digital: + data = self.gpio.DigitalInOut(self.pin_obj).value + else: + data = self.gpio.AnalogIn(self.pin_obj).value + return data diff --git a/sensors/pi/i2c/__init__.py b/sensors/mcp3xxx/__init__.py similarity index 100% rename from sensors/pi/i2c/__init__.py rename to sensors/mcp3xxx/__init__.py diff --git a/sensors/mcp3xxx/sensor.py b/sensors/mcp3xxx/sensor.py new file mode 100644 index 0000000..6b6a4b6 --- /dev/null +++ b/sensors/mcp3xxx/sensor.py @@ -0,0 +1,45 @@ +import adafruit_mcp3xxx.mcp3008 as MCP + +# Base sensor class to extend all other mcp3xxx sensors from. +from sensors.base_sensor import BaseSensor + + +class Sensor(BaseSensor): + PINS = { + 0: MCP.P0, + 1: MCP.P1, + 2: MCP.P2, + 3: MCP.P3, + 4: MCP.P4, + 5: MCP.P5, + 6: MCP.P6, + 7: MCP.P7, + } + + def __init__(self, pin: int, mcp, name=None, key=None, redis_conn=None): + super().__init__( + pin=pin, + name=name, + key=key, + redis_conn=redis_conn + ) + self.mcp = mcp + self.topic = None + + def read_raw(self): + """ + Read the sensor(s) but return the raw voltage, useful for debugging + + Returns: + + """ + return self.topic.voltage + + def read_pin(self): + """ + Read the pin from the mcp3xxx as unaltered digital value + + Returns: + + """ + return self.topic.value diff --git a/sensors/MCP3xxx/soil_sensor.py b/sensors/mcp3xxx/soil_sensor.py similarity index 70% rename from sensors/MCP3xxx/soil_sensor.py rename to sensors/mcp3xxx/soil_sensor.py index 1a84d82..7911c99 100644 --- a/sensors/MCP3xxx/soil_sensor.py +++ b/sensors/mcp3xxx/soil_sensor.py @@ -1,14 +1,11 @@ -import time -import datetime -import json -import redis -from .sensor import Sensor import sys + from adafruit_mcp3xxx.analog_in import AnalogIn from logger.Logger import Logger, LOG_LEVEL +from sensors.mcp3xxx.sensor import Sensor + -sys.path.append('..') # Tested using Sun3Drucker Model SX239 # Wet Water = 287 @@ -21,15 +18,19 @@ class SoilSensor(Sensor): def __init__(self, pin, mcp, name=None, key=None, redis_conn=None): - super().__init__(pin, name=name, key=key, mcp=mcp, redis_conn=redis_conn) + super().__init__(pin, name=name, key=key, mcp=mcp, + redis_conn=redis_conn) return def init_sensor(self): self.topic = AnalogIn(self.mcp, Sensor.PINS[self.pin]) def read(self): - resistance = self.readPin() - moistpercent = ((resistance - WaterBounds) / (AirBounds - WaterBounds)) * 100 + resistance = self.read_pin() + moistpercent = ( + (resistance - WaterBounds) / ( + AirBounds - WaterBounds) + ) * 100 if moistpercent > 80: moisture = 'Very Dry - ' + str(int(moistpercent)) elif 80 >= moistpercent > 45: @@ -41,7 +42,8 @@ def read(self): # print("Resistance: %d" % resistance) # TODO: Put redis store into sensor worker self.r.set(self.key, - resistance) # TODO: CHANGE BACK TO 'moistpercent' (PERSONAL CONFIG) + resistance) + # TODO: CHANGE BACK TO 'moistpercent' (PERSONAL CONFIG) Logger.log(LOG_LEVEL["debug"], "moisture: {0}".format(moisture)) return resistance diff --git a/sensors/pi/float_sensor.py b/sensors/pi/float_sensor.py deleted file mode 100644 index bc29236..0000000 --- a/sensors/pi/float_sensor.py +++ /dev/null @@ -1,28 +0,0 @@ -import time -import json -import redis -from .sensor import Sensor -import RPi.GPIO as GPIO - -#PIN MODE : OUT | IN - -class FloatSensor(Sensor): - - def __init__(self, pin, name=None, key=None, redis_conn=None): - super().__init__(pin, name=name, key=key, redis_conn=redis_conn) - return - - def init_sensor(self): - #Initialize the sensor here (i.e. set pin mode, get addresses, etc) this gets called by the worker - #GPIO.setmode(GPIO.BCM) - GPIO.setup(self.pin, GPIO.IN) - return - - def read(self): - #Read the sensor(s), parse the data and store it in redis if redis is configured - value = GPIO.input(self.pin) - return value - - def readRaw(self): - #Read the sensor(s) but return the raw data, useful for debugging - return self.read() \ No newline at end of file diff --git a/sensors/pi/humidity_sensor.py b/sensors/pi/humidity_sensor.py deleted file mode 100644 index 2006ab2..0000000 --- a/sensors/pi/humidity_sensor.py +++ /dev/null @@ -1,52 +0,0 @@ -import time -import json -import redis -from .sensor import Sensor -import RPi.GPIO as GPIO -import Adafruit_DHT -import sys -sys.path.append('..') - -from logger.Logger import Logger, LOG_LEVEL - -#r = redis.Redis(host='127.0.0.1', port=6379) -#PIN MODE : OUT | IN - -class HumiditySensor(Sensor): - - def __init__(self, pin, name=None, key=None, model='11', redis_conn=None): - super().__init__(pin, name=name, key=key, redis_conn=redis_conn) - self.type = model - return - - def init_sensor(self): - #Initialize the sensor here (i.e. set pin mode, get addresses, etc) this gets called by the worker - sensor_types = { '11': Adafruit_DHT.DHT11, - '22': Adafruit_DHT.DHT22, - '2302': Adafruit_DHT.AM2302 } - if self.type in sensor_types: - self.sensor = sensor_types[self.type] - else: - Logger.log(LOG_LEVEL["warning"], 'Sensor Model Error: Defaulting to DHT11') - self.sensor = Adafruit_DHT.DHT11 - return - - def read(self): - #Read the sensor(s), parse the data and store it in redis if redis is configured - - humidity, temperature = Adafruit_DHT.read_retry(self.sensor, self.pin) - - if humidity is not None and temperature is not None: - self.r.set(self.key + '_temperature', round(temperature * 1.8 + 32, 2)) - self.r.set(self.key + '_humidity', humidity) - readings = {'temperature': round(temperature * 1.8 + 32, 2), 'humidity': round(humidity, 2)} - self.r.set(self.key, json.dumps(readings)) - return readings - else: - Logger.log(LOG_LEVEL["error"], 'DHT Reading was Invalid. Trying again next cycle.') - return None - - - def readRaw(self): - #Read the sensor(s) but return the raw data, useful for debugging - return self.read() diff --git a/sensors/pi/i2c/bme680_sensor.py b/sensors/pi/i2c/bme680_sensor.py deleted file mode 100644 index 4ea1d97..0000000 --- a/sensors/pi/i2c/bme680_sensor.py +++ /dev/null @@ -1,51 +0,0 @@ -import time -import json -import redis -from .sensor import Sensor -import board -from busio import I2C -import adafruit_bme680 - -from logger.Logger import Logger, LOG_LEVEL - -import sys -sys.path.append('..') - -import variables - - -class Bme680Sensor(Sensor): - - def __init__(self, address = None, name=None, key=None, redis_conn=None): - super().__init__(address, name=name, key=key, redis_conn=redis_conn) - return - - def init_sensor(self): - self.sensor = adafruit_bme680.Adafruit_BME680_I2C(self.i2c, debug=False) - # change this to match the location's pressure (hPa) at sea level - self.sensor.sea_level_pressure = 1013.25 - return - - def read(self): - temperature = round((self.sensor.temperature - 5) * 1.8 + 32, 2) - gas = self.sensor.gas - humidity = round(self.sensor.humidity, 1) - pressure = round(self.sensor.pressure, 2) - altitude = round(self.sensor.altitude, 3) - - if humidity is not None and temperature is not None: - self.r.set(self.key + '_temperature', temperature) - self.r.set(self.key + '_humidity', humidity) - self.r.set(self.key + '_gas', gas) - self.r.set(self.key + '_pressure', pressure) - self.r.set(self.key + '_altitude', altitude) - readings = {'temperature': temperature, 'humidity': humidity, 'pressure': pressure, 'gas': gas, 'altitude': altitude} - self.r.set(self.key, json.dumps(readings)) - # print('BME680:', readings) - return readings - else: - Logger.log(LOG_LEVEL["error"], 'Failed to get reading [BME680]. Try again!') - - def readRaw(self): - #Read the sensor(s) but return the raw data, useful for debugging - return self.read() diff --git a/sensors/pi/i2c/sensor.py b/sensors/pi/i2c/sensor.py deleted file mode 100644 index cf9235b..0000000 --- a/sensors/pi/i2c/sensor.py +++ /dev/null @@ -1,51 +0,0 @@ -import time -import json -import redis -import board -from busio import I2C -import RPi.GPIO as GPIO - -#PIN MODE : OUT | IN - -class Sensor(): - - def __init__(self, address, name=None, key=None, redis_conn=None): - self.address = address - - if key is None: - raise Exception('No "key" Found in I2C Sensor Config') - else: - self.key = key.replace(" ", "_").lower() - - if name is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = name - - self.gpio = GPIO - self.i2c = I2C(board.SCL, board.SDA) - try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - return - - def init_sensor(self): - #Initialize the sensor here (i.e. set pin mode, get addresses, etc) - #GPIO.setmode(GPIO.BCM) - #GPIO.setup(pin, GPIO.IN) - pass - - def read(self): - #Read the sensor(s), parse the data and store it in redis if redis is configured - #GPIO.input(pin) - pass - - def readRaw(self): - #Read the sensor(s) but return the raw data, useful for debugging - pass - - def readPin(self): - #Read the pin from the ardiuno. Can be analog or digital based on "analog_pin_mode" - data = self.gpio.input(self.pin) - return data \ No newline at end of file diff --git a/sensors/pi/sensor.py b/sensors/pi/sensor.py deleted file mode 100644 index 657f9c6..0000000 --- a/sensors/pi/sensor.py +++ /dev/null @@ -1,48 +0,0 @@ -import time -import json -import redis -import RPi.GPIO as GPIO - -#PIN MODE : OUT | IN - -class Sensor(): - - def __init__(self, pin, name=None, key=None, redis_conn=None): - self.pin = pin - - if key is None: - raise Exception('No "key" Found in Sensor Config') - else: - self.key = key.replace(" ", "_").lower() - - if name is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = name - - self.gpio = GPIO - try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - return - - def init_sensor(self): - #Initialize the sensor here (i.e. set pin mode, get addresses, etc) - #GPIO.setmode(GPIO.BCM) - #GPIO.setup(pin, GPIO.IN) - pass - - def read(self): - #Read the sensor(s), parse the data and store it in redis if redis is configured - #GPIO.input(pin) - pass - - def readRaw(self): - #Read the sensor(s) but return the raw data, useful for debugging - pass - - def readPin(self): - #Read the pin from the ardiuno. Can be analog or digital based on "analog_pin_mode" - data = self.gpio.input(self.pin) - return data \ No newline at end of file diff --git a/server/mudpi_event_listener.js b/server/mudpi_event_listener.js index 9f58f5b..1f2547e 100644 --- a/server/mudpi_event_listener.js +++ b/server/mudpi_event_listener.js @@ -12,13 +12,13 @@ const axios = require('axios') const address = 'test.php' const channel = '*'; let axiosConfig = { - headers: { + headers: { 'Content-Type': 'application/json;charset=UTF-8', "Access-Control-Allow-Origin": "*", - }, - baseURL: 'http://192.168.2.230/', - timeout: 1000, - responseType: 'json' + }, + baseURL: 'http://192.168.2.230/', + timeout: 1000, + responseType: 'json' }; //------------------------------ @@ -81,22 +81,22 @@ plistener.on('pmessage', (pattern, channel, message) => { console.log(`\x1b[36mPattern Message Received on \x1b[1m${channel}\x1b[0m`); let eventPromise = relayEvent(message) eventPromise.then((response) => { - try { - console.log(`\x1b[32mEvent Successfully Relayed. RESPONSE: %s\x1b[0m`, response.status) - - if(typeof response !== 'undefined' ) { - if (response != null && response.hasOwnProperty('data')) { - console.log('\x1b[32mResponse Data Received: \x1b[0m', response.data) - } - } - } - catch(error) { - console.log('\x1b[32mEvent Successfully Relayed.\x1b[0m \x1b[31mRESPONSE: Error Decoding Response\x1b[0m') - } + try { + console.log(`\x1b[32mEvent Successfully Relayed. RESPONSE: %s\x1b[0m`, response.status) + + if(typeof response !== 'undefined' ) { + if (response != null && response.hasOwnProperty('data')) { + console.log('\x1b[32mResponse Data Received: \x1b[0m', response.data) + } + } + } + catch(error) { + console.log('\x1b[32mEvent Successfully Relayed.\x1b[0m \x1b[31mRESPONSE: Error Decoding Response\x1b[0m') + } }) .catch((error) => { - console.log('\x1b[31mRelaying Event FAILED:\x1b[0m') - console.log(error) + console.log('\x1b[31mRelaying Event FAILED:\x1b[0m') + console.log(error) }) //console.log(data) console.log('\x1b[33mAttempting to Relay Event: \x1b[1m', message, '\x1b[0m') @@ -104,35 +104,35 @@ plistener.on('pmessage', (pattern, channel, message) => { async function relayEvent(event=null) { - let relayedEvent = null - - try { - relayedEvent = await axios.post(address, event, axiosConfig) - } - catch(e) { - if(e.code == 'ENETUNREACH') { - let retries = 3 - while(retries > 1 ) { - - try { - console.log('\x1b[31mRelaying the Event Failed [', e.code, ']\x1b[0m') - await sleep(5000) - console.log('\x1b[33mRetrying Relaying the Event...\x1b[0m') - relayedEvent = await axios.post(address, event, axiosConfig) - } - catch(e) { - console.log('\x1b[31mProblem Resending the Event [', e.code, ']\x1b[0m') - } - - retries-- - } - } - else { - console.log('\x1b[31mProblem Relaying the Event:\x1b[0m') - console.error(e) - } - } - - return relayedEvent + let relayedEvent = null + + try { + relayedEvent = await axios.post(address, event, axiosConfig) + } + catch(e) { + if(e.code == 'ENETUNREACH') { + let retries = 3 + while(retries > 1 ) { + + try { + console.log('\x1b[31mRelaying the Event Failed [', e.code, ']\x1b[0m') + await sleep(5000) + console.log('\x1b[33mRetrying Relaying the Event...\x1b[0m') + relayedEvent = await axios.post(address, event, axiosConfig) + } + catch(e) { + console.log('\x1b[31mProblem Resending the Event [', e.code, ']\x1b[0m') + } + + retries-- + } + } + else { + console.log('\x1b[31mProblem Relaying the Event:\x1b[0m') + console.error(e) + } + } + + return relayedEvent } diff --git a/server/mudpi_server.py b/server/mudpi_server.py index af6ba8f..ce459c2 100644 --- a/server/mudpi_server.py +++ b/server/mudpi_server.py @@ -5,62 +5,80 @@ from logger.Logger import Logger, LOG_LEVEL + # A socket server prototype that was going to be used for devices to communicate. # Instead we are using nodejs to catch events in redis and emit them over a socket. # May update this in later version for device communications. Undetermined. class MudpiServer(object): - def __init__(self, system_running, host='127.0.0.1', port=7002): - self.port = int(port) - self.host = host - self.system_running = system_running - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.client_threads = [] + def __init__(self, system_running, host='127.0.0.1', port=7002): + self.port = int(port) + self.host = host + self.system_running = system_running + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client_threads = [] + + try: + self.sock.bind((self.host, self.port)) + except socket.error as msg: + Logger.log( + LOG_LEVEL["error"], + 'Failed to create socket. Error Code: ', + str(msg[0]), ' , Error Message: ', msg[1] + ) + sys.exit() - try: - self.sock.bind((self.host, self.port)) - except socket.error as msg: - Logger.log(LOG_LEVEL["error"], 'Failed to create socket. Error Code: ', str(msg[0]), ' , Error Message: ', msg[1]) - sys.exit() + def listen(self): + self.sock.listen(10) # number of clients to listen for + Logger.log( + LOG_LEVEL["info"], + 'MudPi Server...\t\t\t\t\033[1;32m Online\033[0;0m ') + while self.system_running.is_set(): + try: + client, address = self.sock.accept() + client.settimeout(60) + Logger.log( + LOG_LEVEL["debug"], 'Client connected from %s', + address + ) + t = threading.Thread(target=self.listen_to_client, + args=(client, address)) + self.client_threads.append(t) + t.start() + except: + pass + print('Server Shutdown...\r', end="", flush=True) + self.sock.close() + Logger.log( + LOG_LEVEL["info"], + 'Server Shutdown...\t\t\t\033[1;32m Complete\033[0;0m' + ) - def listen(self): - self.sock.listen(10) #number of clients to listen for - Logger.log(LOG_LEVEL["info"], 'MudPi Server...\t\t\t\t\033[1;32m Online\033[0;0m ') - while self.system_running.is_set(): - try: - client, address = self.sock.accept() - client.settimeout(60) - Logger.log(LOG_LEVEL["debug"], 'Client connected from ', address) - t = threading.Thread(target = self.listenToClient, args = (client, address)) - self.client_threads.append(t) - t.start() - except: - pass - print('Server Shutdown...\r', end="", flush=True) - self.sock.close() - Logger.log(LOG_LEVEL["info"], 'Server Shutdown...\t\t\t\033[1;32m Complete\033[0;0m') + def listen_to_client(self, client, address): + size = 1024 + while self.system_running.is_set(): + try: + data = pickle.loads(client.recv(size)) + if data: + print(data) + response = data + client.send(pickle.dumps(data)) + else: + raise Exception('Client Disconnected') + except: + client.close() + return False + Logger.log( + LOG_LEVEL["info"], + 'Closing Client Connection...\t\t\033[1;32m Complete\033[0;0m' + ) - def listenToClient(self, client, address): - size = 1024 - while self.system_running.is_set(): - try: - data = pickle.loads(client.recv(size)) - if data: - print(data) - response = data - client.send(pickle.dumps(data)) - else: - raise error('Client Disconnected') - except: - client.close() - return false - Logger.log(LOG_LEVEL["info"], 'Closing Client Connection...\t\t\033[1;32m Complete\033[0;0m') if __name__ == "__main__": - host = '127.0.0.1' - port = 7002 - server = MudpiServer(host, port) - server.listen(); - while True: - pass \ No newline at end of file + host = '127.0.0.1' + port = 7002 + server = MudpiServer(host, port) + server.listen(); + while True: + pass diff --git a/tools/add_lcd_message.py b/tools/add_lcd_message.py index df32557..22b69b5 100644 --- a/tools/add_lcd_message.py +++ b/tools/add_lcd_message.py @@ -9,11 +9,11 @@ old_messages = r.get('lcdmessages') messages = [] if old_messages: - messages = json.loads(old_messages.decode('utf-8')) + messages = json.loads(old_messages.decode('utf-8')) testmessage = {'line_1': line1, 'line_2': line2} messages.append(testmessage) r.set('lcdmessages', json.dumps(messages)) -print('Value set in redis and in queue') \ No newline at end of file +print('Value set in redis and in queue') diff --git a/tools/event_send_tool.py b/tools/event_send_tool.py index c9b7ee0..183fab5 100644 --- a/tools/event_send_tool.py +++ b/tools/event_send_tool.py @@ -3,79 +3,88 @@ import json import time -def timedMessage(message, delay=3): - for s in range(1,delay): - remainingTime = delay - s - print(message + '...{0}s \r'.format(remainingTime), end="", flush=True) - time.sleep(s) -if __name__ == "__main__": - try: - option = True - message = {} - r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True) - publisher = r - topic = None - while option != 0: - #Clear the screen command - print(chr(27) + "[2J") - print('--------- Redis MudPi ---------') - print('|3. Test Event |') - print('|2. Toggle |') - print('|1. Switch |') - print('|0. Shutdown |') - print('-------------------------------') - try: - option = int(input('Enter Option: ')) - except: - #Catch string input error - option = 9 - if option != 0: - if option == 1: - try: - new_state = int(input('Enter State to switch to (0 or 1): ')) - if new_state != 0 and new_state != 1: - new_state = 0 - except: - new_state = 0 - message = { - 'event': 'Switch', - 'data': new_state - } - elif option == 2: - message = { - 'event': 'Toggle', - 'data': None - } - elif option == 3: - message = { - 'event': "StateChanged", - 'data': "/home/pi/Desktop/mudpi/img/mudpi-0039-2019-04-14-02-21.jpg", - 'source': "camera_1" - } - topic = 'garden/pi/camera' - else: - timedMessage('Option not recognized') - print(chr(27) + "[2J") - continue +def timed_message(message, delay=3): + for s in range(1, delay): + remainingTime = delay - s + print(message + '...{0}s \r'.format(remainingTime), end="", flush=True) + time.sleep(s) - if topic is None: - topic = str(input('Enter Topic to Broadcast: ')) - if topic is not None and topic != '': - #Publish the message - publisher.publish(topic, json.dumps(message)) - print(message) - timedMessage('Message Successfully Published!') - else: - timedMessage('Topic Input Invalid') - time.sleep(2) +if __name__ == "__main__": + try: + option = True + message = {} + r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True) + publisher = r + topic = None + while option != 0: + # Clear the screen command + print(chr(27) + "[2J") + print('--------- Redis MudPi ---------') + print('|3. Test Event |') + print('|2. Toggle |') + print('|1. Switch |') + print('|0. Shutdown |') + print('-------------------------------') - print('Exit') - except KeyboardInterrupt: - #Kill The Server - #r.publish('test', json.dumps({'EXIT':True})) - print('Publish Program Terminated...') - finally: - pass - + try: + option = int(input('Enter Option: ')) + except: + # Catch string input error + option = 9 + + if option != 0: + if option == 1: + try: + new_state = int( + input('Enter State to switch to (0 or 1): ')) + if new_state != 0 and new_state != 1: + new_state = 0 + except: + new_state = 0 + message = { + 'event': 'Switch', + 'data': new_state + } + + elif option == 2: + message = { + 'event': 'Toggle', + 'data': None + } + + elif option == 3: + message = { + 'event': "StateChanged", + 'data': "/home/pi/Desktop/mudpi/img/mudpi-0039-2019-04-14-02-21.jpg", + 'source': "camera_1" + } + topic = 'garden/pi/camera' + + else: + timed_message('Option not recognized') + print(chr(27) + "[2J") + continue + + if topic is None: + topic = str(input('Enter Topic to Broadcast: ')) + + if topic is not None and topic != '': + # Publish the message + publisher.publish(topic, json.dumps(message)) + print(message) + timed_message('Message Successfully Published!') + else: + timed_message('Topic Input Invalid') + time.sleep(2) + + print('Exit') + + except KeyboardInterrupt: + # Kill The Server + # r.publish('test', json.dumps({'EXIT':True})) + print('Publish Program Terminated...') + + finally: + pass diff --git a/tools/lcd_message_tool.py b/tools/lcd_message_tool.py index 9e63f1c..78d961a 100644 --- a/tools/lcd_message_tool.py +++ b/tools/lcd_message_tool.py @@ -3,94 +3,94 @@ import json import time -def timedMessage(message, delay=3): - for s in range(1,delay): - remainingTime = delay - s - print(message + '...{0}s \r'.format(remainingTime), end="", flush=True) - time.sleep(s) +def timed_message(message, delay=3): + for s in range(1,delay): + remainingTime = delay - s + print(message + '...{0}s \r'.format(remainingTime), end="", flush=True) + time.sleep(s) if __name__ == "__main__": - try: - option = True - message = {} - r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True) - publisher = r - topic = None - while option != 0: - #Clear the screen command - print(chr(27) + "[2J") - print('--------- LCD MudPi ---------') - print('|4. Clear Message Queue |') - print('|3. Clear Display |') - print('|2. Test Message |') - print('|1. Add Message |') - print('|0. Shutdown |') - print('-------------------------------') - try: - option = int(input('Enter Option: ')) - except: - #Catch string input error - option = 9 - if option != 0: - if option == 1: - try: - msg = { - "message":"", - "duration":10 - } - msg["message"] = str(input('Enter Message to Display: ')) - msg["duration"] = int(input('Enter Duration to Display (seconds): ')) - - except: - msg = { - "message":"Error Test", - "duration":10 - } - message = { - 'event': 'Message', - 'data': msg - } - elif option == 2: - msg = { - "message":"Test Message\nMudPi Test", - "duration":15 - } - message = { - 'event': 'Message', - 'data': msg - } - elif option == 3: - message = { - 'event': 'Clear', - 'data': 1 - } - elif option == 4: - message = { - 'event': 'ClearQueue', - 'data': 1 - } - else: - timedMessage('Option not recognized') - print(chr(27) + "[2J") - continue + try: + option = True + message = {} + r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True) + publisher = r + topic = None + while option != 0: + #Clear the screen command + print(chr(27) + "[2J") + print('--------- LCD MudPi ---------') + print('|4. Clear Message Queue |') + print('|3. Clear Display |') + print('|2. Test Message |') + print('|1. Add Message |') + print('|0. Shutdown |') + print('-------------------------------') + try: + option = int(input('Enter Option: ')) + except: + #Catch string input error + option = 9 + if option != 0: + if option == 1: + try: + msg = { + "message":"", + "duration":10 + } + msg["message"] = str(input('Enter Message to Display: ')) + msg["duration"] = int(input('Enter Duration to Display (seconds): ')) + + except: + msg = { + "message":"Error Test", + "duration":10 + } + message = { + 'event': 'Message', + 'data': msg + } + elif option == 2: + msg = { + "message":"Test Message\nMudPi Test", + "duration":15 + } + message = { + 'event': 'Message', + 'data': msg + } + elif option == 3: + message = { + 'event': 'Clear', + 'data': 1 + } + elif option == 4: + message = { + 'event': 'ClearQueue', + 'data': 1 + } + else: + timed_message('Option not recognized') + print(chr(27) + "[2J") + continue - if topic is None: - topic = str(input('Enter the LCD Topic to Broadcast: ')) + if topic is None: + topic = str(input('Enter the LCD Topic to Broadcast: ')) - if topic is not None and topic != '': - #Publish the message - publisher.publish(topic, json.dumps(message)) - print(message) - timedMessage('Message Successfully Queued!') - else: - timedMessage('Topic Input Invalid') - time.sleep(2) + if topic is not None and topic != '': + #Publish the message + publisher.publish(topic, json.dumps(message)) + print(message) + timed_message('Message Successfully Queued!') + else: + timed_message('Topic Input Invalid') + time.sleep(2) - print('Exit') - except KeyboardInterrupt: - #Kill The Server - #r.publish('test', json.dumps({'EXIT':True})) - print('LCD Message Program Terminated...') - finally: - pass - + print('Exit') + except KeyboardInterrupt: + #Kill The Server + #r.publish('test', json.dumps({'EXIT':True})) + print('LCD Message Program Terminated...') + finally: + pass + diff --git a/tools/lcd_reset.py b/tools/lcd_reset.py index adf0de8..26e774c 100644 --- a/tools/lcd_reset.py +++ b/tools/lcd_reset.py @@ -1,5 +1,5 @@ #!/usr/bin/python - + # The wiring for the LCD is as follows: # 1 : GND # 2 : 5V @@ -17,139 +17,143 @@ # 14: Data Bit 7 # 15: LCD Backlight +5V** # 16: LCD Backlight GND - -#import + +# import import RPi.GPIO as GPIO import time - + # Define GPIO to LCD mapping LCD_RS = 7 -LCD_E = 8 +LCD_E = 8 LCD_D4 = 25 LCD_D5 = 24 LCD_D6 = 23 LCD_D7 = 18 - + # Define some device constants -LCD_WIDTH = 16 # Maximum characters per line +LCD_WIDTH = 16 # Maximum characters per line LCD_CHR = True LCD_CMD = False - -LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line -LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line - + +LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line +LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line + # Timing constants E_PULSE = 0.0005 E_DELAY = 0.0005 + def main(): - - prepare_gpio() - - print('Clearing Lcd...') - # Send some test - lcd_string('LCD SCREEN RESET',LCD_LINE_1) - lcd_string('3 SECONDS LEFT',LCD_LINE_2) - - time.sleep(3) # 3 second delay - - lcd_byte(0x01, LCD_CMD) - lcd_string("RESET IN PROGRES",LCD_LINE_1) - lcd_string("Shutting Down: 1s",LCD_LINE_2) - time.sleep(1) - lcd_string(" ",LCD_LINE_1) - lcd_string(" ",LCD_LINE_2) - time.sleep(E_DELAY) - lcd_byte(0x01, LCD_CMD) - time.sleep(E_DELAY) - lcd_byte(0x01, LCD_CMD) - time.sleep(E_DELAY) - GPIO.cleanup() + prepare_gpio() + + print('Clearing Lcd...') + # Send some test + lcd_string('LCD SCREEN RESET', LCD_LINE_1) + lcd_string('3 SECONDS LEFT', LCD_LINE_2) + + time.sleep(3) # 3 second delay + + lcd_byte(0x01, LCD_CMD) + lcd_string("RESET IN PROGRES", LCD_LINE_1) + lcd_string("Shutting Down: 1s", LCD_LINE_2) + time.sleep(1) + lcd_string(" ", LCD_LINE_1) + lcd_string(" ", LCD_LINE_2) + time.sleep(E_DELAY) + lcd_byte(0x01, LCD_CMD) + time.sleep(E_DELAY) + lcd_byte(0x01, LCD_CMD) + time.sleep(E_DELAY) + GPIO.cleanup() def prepare_gpio(): - # Main program block - GPIO.setwarnings(False) - GPIO.setmode(GPIO.BCM) # Use BCM GPIO numbers - GPIO.setup(LCD_E, GPIO.OUT) # E - GPIO.setup(LCD_RS, GPIO.OUT) # RS - GPIO.setup(LCD_D4, GPIO.OUT) # DB4 - GPIO.setup(LCD_D5, GPIO.OUT) # DB5 - GPIO.setup(LCD_D6, GPIO.OUT) # DB6 - GPIO.setup(LCD_D7, GPIO.OUT) # DB7 - # Initialise display - lcd_init() - - + # Main program block + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) # Use BCM GPIO numbers + GPIO.setup(LCD_E, GPIO.OUT) # E + GPIO.setup(LCD_RS, GPIO.OUT) # RS + GPIO.setup(LCD_D4, GPIO.OUT) # DB4 + GPIO.setup(LCD_D5, GPIO.OUT) # DB5 + GPIO.setup(LCD_D6, GPIO.OUT) # DB6 + GPIO.setup(LCD_D7, GPIO.OUT) # DB7 + # Initialise display + lcd_init() + + def lcd_init(): - # Initialise display - lcd_byte(0x33,LCD_CMD) # 110011 Initialise - lcd_byte(0x32,LCD_CMD) # 110010 Initialise - lcd_byte(0x06,LCD_CMD) # 000110 Cursor move direction - lcd_byte(0x0C,LCD_CMD) # 001100 Display On,Cursor Off, Blink Off - lcd_byte(0x28,LCD_CMD) # 101000 Data length, number of lines, font size - lcd_byte(0x01,LCD_CMD) # 000001 Clear display - time.sleep(E_DELAY) - + # Initialise display + lcd_byte(0x33, LCD_CMD) # 110011 Initialise + lcd_byte(0x32, LCD_CMD) # 110010 Initialise + lcd_byte(0x06, LCD_CMD) # 000110 Cursor move direction + lcd_byte(0x0C, LCD_CMD) # 001100 Display On,Cursor Off, Blink Off + lcd_byte(0x28, LCD_CMD) # 101000 Data length, number of lines, font size + lcd_byte(0x01, LCD_CMD) # 000001 Clear display + time.sleep(E_DELAY) + + def lcd_byte(bits, mode): - # Send byte to data pins - # bits = data - # mode = True for character - # False for command - - GPIO.output(LCD_RS, mode) # RS - - # High bits - GPIO.output(LCD_D4, False) - GPIO.output(LCD_D5, False) - GPIO.output(LCD_D6, False) - GPIO.output(LCD_D7, False) - if bits&0x10==0x10: - GPIO.output(LCD_D4, True) - if bits&0x20==0x20: - GPIO.output(LCD_D5, True) - if bits&0x40==0x40: - GPIO.output(LCD_D6, True) - if bits&0x80==0x80: - GPIO.output(LCD_D7, True) - - # Toggle 'Enable' pin - lcd_toggle_enable() - - # Low bits - GPIO.output(LCD_D4, False) - GPIO.output(LCD_D5, False) - GPIO.output(LCD_D6, False) - GPIO.output(LCD_D7, False) - if bits&0x01==0x01: - GPIO.output(LCD_D4, True) - if bits&0x02==0x02: - GPIO.output(LCD_D5, True) - if bits&0x04==0x04: - GPIO.output(LCD_D6, True) - if bits&0x08==0x08: - GPIO.output(LCD_D7, True) - - # Toggle 'Enable' pin - lcd_toggle_enable() - + # Send byte to data pins + # bits = data + # mode = True for character + # False for command + + GPIO.output(LCD_RS, mode) # RS + + # High bits + GPIO.output(LCD_D4, False) + GPIO.output(LCD_D5, False) + GPIO.output(LCD_D6, False) + GPIO.output(LCD_D7, False) + if bits & 0x10 == 0x10: + GPIO.output(LCD_D4, True) + if bits & 0x20 == 0x20: + GPIO.output(LCD_D5, True) + if bits & 0x40 == 0x40: + GPIO.output(LCD_D6, True) + if bits & 0x80 == 0x80: + GPIO.output(LCD_D7, True) + + # Toggle 'Enable' pin + lcd_toggle_enable() + + # Low bits + GPIO.output(LCD_D4, False) + GPIO.output(LCD_D5, False) + GPIO.output(LCD_D6, False) + GPIO.output(LCD_D7, False) + if bits & 0x01 == 0x01: + GPIO.output(LCD_D4, True) + if bits & 0x02 == 0x02: + GPIO.output(LCD_D5, True) + if bits & 0x04 == 0x04: + GPIO.output(LCD_D6, True) + if bits & 0x08 == 0x08: + GPIO.output(LCD_D7, True) + + # Toggle 'Enable' pin + lcd_toggle_enable() + + def lcd_toggle_enable(): - # Toggle enable - time.sleep(E_DELAY) - GPIO.output(LCD_E, True) - time.sleep(E_PULSE) - GPIO.output(LCD_E, False) - time.sleep(E_DELAY) - -def lcd_string(message,line): - # Send string to display - - message = message.ljust(LCD_WIDTH," ") - - lcd_byte(line, LCD_CMD) - - for i in range(LCD_WIDTH): - lcd_byte(ord(message[i]),LCD_CHR) - + # Toggle enable + time.sleep(E_DELAY) + GPIO.output(LCD_E, True) + time.sleep(E_PULSE) + GPIO.output(LCD_E, False) + time.sleep(E_DELAY) + + +def lcd_string(message, line): + # Send string to display + + message = message.ljust(LCD_WIDTH, " ") + + lcd_byte(line, LCD_CMD) + + for i in range(LCD_WIDTH): + lcd_byte(ord(message[i]), LCD_CHR) + + if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/triggers/control_trigger.py b/triggers/control_trigger.py index 4943ad6..f1da0a0 100644 --- a/triggers/control_trigger.py +++ b/triggers/control_trigger.py @@ -3,63 +3,92 @@ import redis import sys from .trigger import Trigger -sys.path.append('..') + + from logger.Logger import Logger, LOG_LEVEL + class ControlTrigger(Trigger): - def __init__(self, main_thread_running, system_ready, name='ControlTrigger',key=None, source=None, thresholds=None, topic="controls", trigger_active=None, frequency='once', actions=[], group=None, redis_conn=None, sequences=[]): - super().__init__(main_thread_running, system_ready, name=name, key=key, source=source, thresholds=thresholds, trigger_active=trigger_active, frequency=frequency, actions=actions, trigger_interval=0.5, group=group, sequences=sequences) - self.topic = topic.replace(" ", "_").lower() if topic is not None else "controls" - try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - return + def __init__( + self, main_thread_running, system_ready, name='ControlTrigger', + key=None, source=None, thresholds=None, topic="controls", + trigger_active=None, frequency='once', actions=[], + group=None, redis_conn=None, sequences=[]): + super().__init__( + main_thread_running, + system_ready, + name=name, + key=key, + source=source, + thresholds=thresholds, + trigger_active=trigger_active, + frequency=frequency, + actions=actions, + trigger_interval=0.5, + group=group, + sequences=sequences + ) + self.topic = topic.replace(" ", + "_").lower() if topic is not None else "controls" + + try: + self.r = redis_conn if redis_conn is not None else redis.Redis( + host='127.0.0.1', port=6379) + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + return + + def init_trigger(self): + # Initialize the trigger here (i.e. set listeners or create cron jobs) + # Pubsub Listeners + self.pubsub = self.r.pubsub() + self.pubsub.subscribe(**{self.topic: self.handle_event}) + pass + + def check(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + super().check() + self.pubsub.get_message() + # self.trigger_active.clear() + time.sleep(self.trigger_interval) + else: + time.sleep(2) + return - def init_trigger(self): - #Initialize the trigger here (i.e. set listeners or create cron jobs) - #Pubsub Listeners - self.pubsub = self.r.pubsub() - self.pubsub.subscribe(**{self.topic: self.handleEvent}) - pass + def handle_event(self, message): + data = message['data'] + if data is not None: + decoded_message = super().decode_event_data(data) - def check(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - super().check() - self.pubsub.get_message() - # self.trigger_active.clear() - time.sleep(self.trigger_interval) - else: - time.sleep(2) - return + try: + if decoded_message['event'] == 'ControlUpdate': + control_value = self.parse_control_data( + decoded_message["data"] + ) + if super().evaluate_thresholds(control_value): + self.trigger_active.set() + if self.previous_state != self.trigger_active.is_set(): + super().trigger(decoded_message['event']) + else: + if self.frequency == 'many': + super().trigger(decoded_message['event']) + else: + self.trigger_active.clear() - def handleEvent(self, message): - data = message['data'] - if data is not None: - decoded_message = super().decodeEventData(data) - try: - if decoded_message['event'] == 'ControlUpdate': - control_value = self.parseControlData(decoded_message["data"]) - if super().evaluateThresholds(control_value): - self.trigger_active.set() - if self.previous_state != self.trigger_active.is_set(): - super().trigger(decoded_message['event']) - else: - if self.frequency == 'many': - super().trigger(decoded_message['event']) - else: - self.trigger_active.clear() - except: - Logger.log(LOG_LEVEL["error"], 'Error During Trigger Actions {0}'.format(self.key)) - self.previous_state = self.trigger_active.is_set() + except: + Logger.log( + LOG_LEVEL["error"], + 'Error During Trigger Actions {0}'.format(self.key) + ) + self.previous_state = self.trigger_active.is_set() - def parseControlData(self, data): - parsed_data = data.get(self.source, None) - return parsed_data + def parse_control_data(self, data): + parsed_data = data.get(self.source, None) + return parsed_data - def shutdown(self): - self.pubsub.close() - return \ No newline at end of file + def shutdown(self): + self.pubsub.close() + return diff --git a/triggers/sensor_trigger.py b/triggers/sensor_trigger.py index 5bd3281..f0fa108 100644 --- a/triggers/sensor_trigger.py +++ b/triggers/sensor_trigger.py @@ -3,64 +3,91 @@ import redis import sys from .trigger import Trigger -sys.path.append('..') + + from logger.Logger import Logger, LOG_LEVEL + class SensorTrigger(Trigger): - def __init__(self, main_thread_running, system_ready, name='SensorTrigger',key=None, source=None, nested_source=None, thresholds=None, topic="sensors", trigger_active=None, frequency='once', actions=[], group=None, redis_conn=None, sequences=[]): - super().__init__(main_thread_running, system_ready, name=name, key=key, source=source, thresholds=thresholds, trigger_active=trigger_active, frequency=frequency, actions=actions, trigger_interval=0.5, group=group, sequences=sequences) - self.topic = topic.replace(" ", "_").lower() if topic is not None else "sensors" - self.nested_source = nested_source.lower() if nested_source is not None else nested_source - try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - return + def __init__( + self, main_thread_running, system_ready, name='SensorTrigger', + key=None, source=None, nested_source=None, thresholds=None, + topic="sensors", trigger_active=None, frequency='once', + actions=[], group=None, redis_conn=None, sequences=[]): + super().__init__( + main_thread_running, + system_ready, + name=name, + key=key, + source=source, + thresholds=thresholds, + trigger_active=trigger_active, + frequency=frequency, + actions=actions, + trigger_interval=0.5, + group=group, + sequences=sequences + ) + self.topic = topic.replace(" ", + "_").lower() if topic is not None else "sensors" + self.nested_source = nested_source.lower() if nested_source is not None else nested_source + try: + self.r = redis_conn if redis_conn is not None else redis.Redis( + host='127.0.0.1', port=6379) + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + return - def init_trigger(self): - #Initialize the trigger here (i.e. set listeners or create cron jobs) - #Pubsub Listeners - self.pubsub = self.r.pubsub() - self.pubsub.subscribe(**{self.topic: self.handleEvent}) - pass + def init_trigger(self): + # Initialize the trigger here (i.e. set listeners or create cron jobs) + # Pubsub Listeners + self.pubsub = self.r.pubsub() + self.pubsub.subscribe(**{self.topic: self.handle_event}) + pass - def check(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - super().check() - self.pubsub.get_message() - # self.trigger_active.clear() - time.sleep(self.trigger_interval) - else: - time.sleep(2) - return + def check(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + super().check() + self.pubsub.get_message() + # self.trigger_active.clear() + time.sleep(self.trigger_interval) + else: + time.sleep(2) + return - def handleEvent(self, message): - data = message['data'] - if data is not None: - decoded_message = super().decodeEventData(data) - try: - if decoded_message['event'] == 'SensorUpdate' or decoded_message['event'] == 'PiSensorUpdate': - sensor_value = self.parseSensorData(decoded_message["data"]) - if super().evaluateThresholds(sensor_value): - self.trigger_active.set() - if self.previous_state != self.trigger_active.is_set(): - super().trigger(decoded_message) - else: - if self.frequency == 'many': - super().trigger(decoded_message) - else: - self.trigger_active.clear() - except: - Logger.log(LOG_LEVEL["error"], 'Error Triggering Actions for {0}'.format(self.name)) - self.previous_state = self.trigger_active.is_set() + def handle_event(self, message): + data = message['data'] + if data is not None: + decoded_message = super().decode_event_data(data) + try: + if decoded_message['event'] == 'SensorUpdate' or \ + decoded_message['event'] == 'PiSensorUpdate': + sensor_value = self.parse_sensor_data( + decoded_message["data"]) + if super().evaluate_thresholds(sensor_value): + self.trigger_active.set() + if self.previous_state != self.trigger_active.is_set(): + super().trigger(decoded_message) + else: + if self.frequency == 'many': + super().trigger(decoded_message) + else: + self.trigger_active.clear() + except: + Logger.log(LOG_LEVEL["error"], + 'Error Triggering Actions for {0}'.format( + self.name)) + self.previous_state = self.trigger_active.is_set() - def parseSensorData(self, data): - parsed_data = data.get(self.source).get(self.nested_source, None) if self.nested_source is not None else data.get(self.source, None) - return parsed_data + def parse_sensor_data(self, data): + parsed_data = data.get(self.source).get(self.nested_source, + None) if self.nested_source is not None else data.get( + self.source, None) + return parsed_data - def shutdown(self): - self.pubsub.close() - return \ No newline at end of file + def shutdown(self): + self.pubsub.close() + return diff --git a/triggers/time_trigger.py b/triggers/time_trigger.py index e33f9d3..fecdb22 100644 --- a/triggers/time_trigger.py +++ b/triggers/time_trigger.py @@ -3,45 +3,67 @@ import redis import sys from .trigger import Trigger + try: - import pycron - CRON_ENABLED = True + import pycron + + CRON_ENABLED = True except ImportError: - CRON_ENABLED = False -sys.path.append('..') + CRON_ENABLED = False + from logger.Logger import Logger, LOG_LEVEL + class TimeTrigger(Trigger): - def __init__(self, main_thread_running, system_ready, name='TimeTrigger',key=None, trigger_active=None, actions=[], schedule=None, group=None, sequences=[]): - super().__init__(main_thread_running, system_ready, name=name, key=key, trigger_active=trigger_active, actions=actions, trigger_interval=60, group=group, sequences=sequences) - self.schedule = schedule - return - - def init_trigger(self): - #Initialize the trigger here (i.e. set listeners or create cron jobs) - pass - - def check(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - super().check() - try: - if CRON_ENABLED: - if pycron.is_now(self.schedule): - self.trigger_active.set() - super().trigger() - else: - self.trigger_active.clear() - else: - Logger.log(LOG_LEVEL["error"], "Error pycron not found.") - except: - Logger.log(LOG_LEVEL["error"], "Error evaluating time trigger schedule.") - time.sleep(self.trigger_interval) - else: - time.sleep(2) - return - - def shutdown(self): - return \ No newline at end of file + def __init__( + self, main_thread_running, system_ready, name='TimeTrigger', + key=None, trigger_active=None, actions=[], schedule=None, + group=None, sequences=[]): + super().__init__( + main_thread_running, + system_ready, + name=name, + key=key, + trigger_active=trigger_active, + actions=actions, + trigger_interval=60, + group=group, + sequences=sequences + ) + self.schedule = schedule + return + + def init_trigger(self): + # Initialize the trigger here (i.e. set listeners or create cron jobs) + pass + + def check(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + super().check() + try: + if CRON_ENABLED: + if pycron.is_now(self.schedule): + self.trigger_active.set() + super().trigger() + else: + self.trigger_active.clear() + else: + Logger.log( + LOG_LEVEL["error"], + "Error pycron not found." + ) + except: + Logger.log( + LOG_LEVEL["error"], + "Error evaluating time trigger schedule." + ) + time.sleep(self.trigger_interval) + else: + time.sleep(2) + return + + def shutdown(self): + return diff --git a/triggers/trigger.py b/triggers/trigger.py index 780092b..4e711e7 100644 --- a/triggers/trigger.py +++ b/triggers/trigger.py @@ -3,123 +3,138 @@ import redis import threading import sys -sys.path.append('..') + + from logger.Logger import Logger, LOG_LEVEL + class Trigger(): - def __init__(self, main_thread_running, system_ready, name=None, key=None, source=None, thresholds=None, trigger_active=None, frequency='once', actions=[], trigger_interval=1, group=None, sequences=[]): - - if key is None: - raise Exception('No "key" Found in Trigger Config') - else: - self.key = key.replace(" ", "_").lower() - - if name is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = name - - self.thresholds = thresholds - self.source = source.lower() if source is not None else source - self.trigger_interval = trigger_interval - self.actions = actions - self.sequences = sequences - self.group = group - self.frequency = frequency if group is None else "many" - # Used to check if trigger already fired without reseting - self.trigger_active = trigger_active - self.previous_state = trigger_active.is_set() - # Main thread events - self.main_thread_running = main_thread_running - self.system_ready = system_ready - return - - def init_trigger(self): - # Initialize the trigger here (i.e. set listeners or create cron jobs) - pass - - def check(self): - # Main trigger check loop to do things like fetch messages or check time - if self.group is not None: - self.group.check_group() - return - - def run(self): - t = threading.Thread(target=self.check, args=()) - t.start() - return t - - def trigger(self, value=None): - try: - if self.group is None: - # Trigger the actions of the trigger - for action in self.actions: - action.trigger(value) - # Trigger the sequences of the trigger - for sequence in self.sequences: - sequence.update(value) - else: - self.group.trigger() - except Exception as e: - Logger.log(LOG_LEVEL["error"], "Error triggering action {0} ".format(self.key), e) - pass - return - - - def evaluateThresholds(self, value): - thresholds_passed = False - for threshold in self.thresholds: - comparison = threshold.get("comparison", "eq") - if comparison == "eq": - if value == threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "ne": - if value != threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "gt": - if value > threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "gte": - if value >= threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "lt": - if value < threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "lte": - if value <= threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - return thresholds_passed - - def decodeEventData(self, message): - if isinstance(message, dict): - #print('Dict Found') - return message - elif isinstance(message.decode('utf-8'), str): - try: - temp = json.loads(message.decode('utf-8')) - #print('Json Found') - return temp - except: - #print('Json Error. Str Found') - return {'event':'Unknown', 'data':message} - else: - #print('Failed to detect type') - return {'event':'Unknown', 'data':message} - - def shutdown(self): - # Put any closing functions here that should be called as MudPi shutsdown (i.e. close connections) - return \ No newline at end of file + def __init__(self, main_thread_running, system_ready, name=None, key=None, + source=None, thresholds=None, trigger_active=None, + frequency='once', actions=[], trigger_interval=1, group=None, + sequences=[]): + + if key is None: + raise Exception('No "key" Found in Trigger Config') + else: + self.key = key.replace(" ", "_").lower() + + if name is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = name + + self.thresholds = thresholds + self.source = source.lower() if source is not None else source + self.trigger_interval = trigger_interval + self.actions = actions + self.sequences = sequences + self.group = group + self.frequency = frequency if group is None else "many" + # Used to check if trigger already fired without reseting + self.trigger_active = trigger_active + self.previous_state = trigger_active.is_set() + # Main thread events + self.main_thread_running = main_thread_running + self.system_ready = system_ready + return + + def init_trigger(self): + # Initialize the trigger here (i.e. set listeners or create cron jobs) + pass + + def check(self): + # Main trigger check loop to do things like + # fetch messages or check time + if self.group is not None: + self.group.check_group() + return + + def run(self): + t = threading.Thread(target=self.check, args=()) + t.start() + return t + + def trigger(self, value=None): + try: + if self.group is None: + # Trigger the actions of the trigger + for action in self.actions: + action.trigger(value) + # Trigger the sequences of the trigger + for sequence in self.sequences: + sequence.update(value) + else: + self.group.trigger() + except Exception as e: + Logger.log(LOG_LEVEL["error"], + "Error triggering action {0} ".format(self.key), e) + pass + return + + def evaluate_thresholds(self, value): + thresholds_passed = False + for threshold in self.thresholds: + comparison = threshold.get("comparison", "eq") + if comparison == "eq": + if value == threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + + elif comparison == "ne": + if value != threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + + elif comparison == "gt": + if value > threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + + elif comparison == "gte": + if value >= threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + + elif comparison == "lt": + if value < threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + + elif comparison == "lte": + if value <= threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + + return thresholds_passed + + def decode_event_data(self, message): + if isinstance(message, dict): + # print('Dict Found') + return message + + elif isinstance(message.decode('utf-8'), str): + try: + temp = json.loads(message.decode('utf-8')) + # print('Json Found') + return temp + except: + # print('Json Error. Str Found') + return {'event': 'Unknown', 'data': message} + + else: + # print('Failed to detect type') + return {'event': 'Unknown', 'data': message} + + def shutdown(self): + # Put any closing functions here that should be called as + # MudPi shutsdown (i.e. close connections) + return diff --git a/triggers/trigger_group.py b/triggers/trigger_group.py index 192c0e4..86e80cc 100644 --- a/triggers/trigger_group.py +++ b/triggers/trigger_group.py @@ -3,70 +3,82 @@ import redis import threading import sys -sys.path.append('..') + + from logger.Logger import Logger, LOG_LEVEL + class TriggerGroup(): - def __init__(self, name='TriggerGroup', key=None, triggers=[], group_active=None, frequency='once', actions=[], sequences=[]): - if key is None: - raise Exception('No "key" Found in Trigger Group Config') - else: - self.key = key.replace(" ", "_").lower() + def __init__(self, name='TriggerGroup', key=None, triggers=[], + group_active=None, frequency='once', actions=[], + sequences=[]): + if key is None: + raise Exception('No "key" Found in Trigger Group Config') + else: + self.key = key.replace(" ", "_").lower() + + if name is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = name + self.frequency = frequency + self.actions = actions + # Used to check if trigger already fired without reseting + self.group_active = group_active if group_active is not None else threading.Event() + self.previous_state = self.group_active.is_set() + self.trigger_count = 0 + self.triggers = triggers + return + + def add_trigger(self, trigger): + self.triggers.append(trigger) + pass + + def check_group(self): + group_check = True + for trigger in self.triggers: + if not trigger.trigger_active.is_set(): + group_check = False + if group_check: + self.group_active.set() + else: + self.group_active.clear() + self.trigger_count = 0 + self.previous_state = self.group_active.is_set() + return group_check + + def trigger(self, value=None): + try: + if self.check_group(): + self.trigger_count += 1 + if self.trigger_count == 1: - if name is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = name - self.frequency = frequency - self.actions = actions - # Used to check if trigger already fired without reseting - self.group_active = group_active if group_active is not None else threading.Event() - self.previous_state = self.group_active.is_set() - self.trigger_count = 0 - self.triggers = triggers - return + for action in self.actions: + action.trigger(value) - def add_trigger(self, trigger): - self.triggers.append(trigger) - pass + for sequence in self.sequences: + sequence.update(value) - def check_group(self): - group_check = True - for trigger in self.triggers: - if not trigger.trigger_active.is_set(): - group_check = False - if group_check: - self.group_active.set() - else: - self.group_active.clear() - self.trigger_count = 0 - self.previous_state = self.group_active.is_set() - return group_check + else: + if self.frequency == 'many': + for action in self.actions: + action.trigger(value) - def trigger(self, value=None): - try: - if self.check_group(): - self.trigger_count+=1 - if self.trigger_count == 1: - for action in self.actions: - action.trigger(value) - for sequence in self.sequences: - sequence.update(value) - else: - if self.frequency == 'many': - for action in self.actions: - action.trigger(value) - for sequence in self.sequences: - sequence.update(value) - else: - self.trigger_count = 0 - except Exception as e: - Logger.log(LOG_LEVEL["error"], "Error triggering group {0} ".format(self.key), e) - pass - return + for sequence in self.sequences: + sequence.update(value) + else: + self.trigger_count = 0 + except Exception as e: + Logger.log( + LOG_LEVEL["error"], + "Error triggering group {0} ".format(self.key), e + ) + pass + return - def shutdown(self): - #Put any closing functions here that should be called as MudPi shutsdown (i.e. close connections) - return \ No newline at end of file + def shutdown(self): + # Put any closing functions here that should be called as + # MudPi shutsdown (i.e. close connections) + return diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..66dc4fa --- /dev/null +++ b/utils.py @@ -0,0 +1,35 @@ +import json + + +def load_config_json(): + """ + + Returns: + Dictionary from json file. + """ + + with open('mudpi.config') as loadedfile: + configs = json.load(loadedfile) + loadedfile.close() + return configs + + +def get_config_item(config, item, default=None, replace_char='_'): + """ + + Args: + config: + item: + default: + replace_char: + + Returns: + Configuration item + """ + + value = config.get(item, default) + + if type(value) == str: + value = value.replace(" ", replace_char).lower() + + return value diff --git a/variables.py b/variables.py deleted file mode 100644 index 1388c2d..0000000 --- a/variables.py +++ /dev/null @@ -1,5 +0,0 @@ -PREVIOUS_LINE="\x1b[1F" -RED_BACK="\x1b[41;37m" -GREEN_BACK="\x1b[42;30m" -YELLOW_BACK="\x1b[43;30m" -RESET="\x1b[0m" diff --git a/workers/adc_worker.py b/workers/adc_worker.py index cc5654f..f9cfc42 100644 --- a/workers/adc_worker.py +++ b/workers/adc_worker.py @@ -41,7 +41,8 @@ def __init__(self, config: dict, main_thread_running, system_ready): self.system_ready = system_ready self.node_ready = False try: - self.r = redis_conn if redis_conn is not None else redis.Redis(host='127.0.0.1', port=6379) + self.r = redis_conn if redis_conn is not None else redis.Redis( + host='127.0.0.1', port=6379) except KeyError: self.r = redis.Redis(host='127.0.0.1', port=6379) @@ -71,28 +72,45 @@ def init_sensors(self): for sensor in self.config['sensors']: if sensor.get('type', None) is not None: - # Get the sensor from the sensors folder {sensor name}_sensor.{SensorName}Sensor - sensor_type = 'sensors.MCP3xxx.' + sensor.get('type').lower() + '_sensor.' + sensor.get( + # Get the sensor from the sensors folder + # {sensor name}_sensor.{SensorName}Sensor + sensor_type = 'sensors.mcp3xxx.' + sensor.get( + 'type').lower() + '_sensor.' + sensor.get( 'type').capitalize() + 'Sensor' # analog_pin_mode = False if sensor.get('is_digital', False) else True imported_sensor = self.dynamic_sensor_import(sensor_type) new_sensor = imported_sensor(int(sensor.get('pin')), - name=sensor.get('name', sensor.get('type')), + name=sensor.get('name', + sensor.get( + 'type')), key=sensor.get('key', None), mcp=self.mcp) new_sensor.init_sensor() self.sensors.append(new_sensor) - Logger.log(LOG_LEVEL["info"], '{type} Sensor {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format(**sensor)) + Logger.log( + LOG_LEVEL["info"], + '{type} Sensor {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format( + **sensor) + ) def run(self): + if self.node_ready: t = threading.Thread(target=self.work, args=()) t.start() - Logger.log(LOG_LEVEL["info"], str(self.config['name']) + ' Node Worker [' + str( - len(self.config['sensors'])) + ' Sensors]...\t\033[1;32m Online\033[0;0m') + Logger.log( + LOG_LEVEL["info"], + str(self.config['name']) + ' Node Worker [' + str( + len(self.config[ + 'sensors'])) + ' Sensors]...\t\033[1;32m Online\033[0;0m' + ) return t + else: - Logger.log(LOG_LEVEL["warning"], "Node Connection...\t\t\t\033[1;31m Failed\033[0;0m") + Logger.log( + LOG_LEVEL["warning"], + "Node Connection...\t\t\t\033[1;31m Failed\033[0;0m" + ) return None def work(self): @@ -112,5 +130,8 @@ def work(self): time.sleep(15) # This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "{name} Node Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m".format( - **self.config)) + Logger.log( + LOG_LEVEL["info"], + "{name} Node Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m".format( + **self.config) + ) diff --git a/workers/arduino/arduino_control_worker.py b/workers/arduino/arduino_control_worker.py index a182125..bd07761 100644 --- a/workers/arduino/arduino_control_worker.py +++ b/workers/arduino/arduino_control_worker.py @@ -11,96 +11,118 @@ from controls.arduino.switch_control import (SwitchControl) from controls.arduino.potentiometer_control import (PotentiometerControl) import sys -sys.path.append('..') -import variables + + +import constants import importlib from logger.Logger import Logger, LOG_LEVEL -#r = redis.Redis(host='127.0.0.1', port=6379) + +# r = redis.Redis(host='127.0.0.1', port=6379) class ArduinoControlWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, node_connected, connection=None): - super().__init__(config, main_thread_running, system_ready) - self.controls_ready = False - self.node_connected = node_connected - self.connection = connection - - self.controls = [] - - if node_connected.is_set(): - self.init() - self.controls_ready = True - return - - - def init(self): - try: - for control in self.config['controls']: - if control.get('type', None) is not None: - # Get the control from the controls folder {control name}_control.{ControlName}Control - control_type = 'controls.arduino.' + control.get('type').lower() + '_control.' + control.get('type').capitalize() + 'Control' - - analog_pin_mode = False if control.get('is_digital', True) else True - - imported_control = self.dynamic_import(control_type) - - # Define default kwargs for all control types, conditionally include optional variables below if they exist - control_kwargs = { - 'name' : control.get('name', None), - 'pin' : int(control.get('pin')), - 'connection': self.connection, - 'key' : control.get('key', None), - 'analog_pin_mode': analog_pin_mode, - 'topic': control.get('topic', None) - } - - # optional control variables - # add conditional control vars here... - - new_control = imported_control(**control_kwargs) - - new_control.init_control() - self.controls.append(new_control) - self.controls_ready = True - print('{type} Control {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format(**control)) - except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: - # Connection error. Reset everything for reconnect - self.controls_ready = False - self.node_connected.clear() - self.controls = [] - return - - def run(self): - t = threading.Thread(target=self.work, args=()) - t.start() - return t - - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - if self.node_connected.is_set(): - if self.controls_ready: - try: - readings = {} - for control in self.controls: - result = control.read() - readings[control.key] = result - except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: - Logger.log(LOG_LEVEL["warning"], '\033[1;36m{name}\033[0;0m -> \033[1;33mControls Timeout!\033[0;0m'.format(**self.config)) - self.controls_ready = False - self.controls = [] - self.node_connected.clear() - time.sleep(15) - else: - # Worker connected but controls not initialized - self.init() - else: - # Node not connected. Wait for reconnect - self.controls_ready = False - self.controls = [] - time.sleep(10) - #Will this nuke the connection? - time.sleep(0.05) - #This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "{name} Controls Shutting Down...\t\033[1;32m Complete\033[0;0m".format(**self.config)) \ No newline at end of file + def __init__(self, config, main_thread_running, system_ready, + node_connected, connection=None): + super().__init__(config, main_thread_running, system_ready) + self.controls_ready = False + self.node_connected = node_connected + self.connection = connection + + self.controls = [] + + if node_connected.is_set(): + self.init() + self.controls_ready = True + return + + def init(self): + try: + for control in self.config['controls']: + if control.get('type', None) is not None: + # Get the control from the controls folder + # {control name}_control.{ControlName}Control + control_type = 'controls.arduino.' + control.get( + 'type').lower() + '_control.' + control.get( + 'type').capitalize() + 'Control' + + analog_pin_mode = False if control.get('is_digital', + True) else True + + imported_control = self.dynamic_import(control_type) + + # Define default kwargs for all control types, + # conditionally include optional variables below + # if they exist + control_kwargs = { + 'name': control.get('name', None), + 'pin': int(control.get('pin')), + 'connection': self.connection, + 'key': control.get('key', None), + 'analog_pin_mode': analog_pin_mode, + 'topic': control.get('topic', None) + } + + # optional control variables + # add conditional control vars here... + + new_control = imported_control(**control_kwargs) + + new_control.init_control() + self.controls.append(new_control) + self.controls_ready = True + print( + '{type} Control {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format( + **control) + ) + except (SerialManagerError, SocketManagerError, BrokenPipeError, + ConnectionResetError, OSError, socket.timeout) as e: + # Connection error. Reset everything for reconnect + self.controls_ready = False + self.node_connected.clear() + self.controls = [] + return + + def run(self): + t = threading.Thread(target=self.work, args=()) + t.start() + return t + + def work(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + if self.node_connected.is_set(): + if self.controls_ready: + try: + readings = {} + for control in self.controls: + result = control.read() + readings[control.key] = result + except (SerialManagerError, SocketManagerError, + BrokenPipeError, ConnectionResetError, OSError, + socket.timeout) as e: + Logger.log( + LOG_LEVEL["warning"], + '\033[1;36m{name}\033[0;0m -> \033[1;33mControls Timeout!\033[0;0m'.format( + **self.config) + ) + self.controls_ready = False + self.controls = [] + self.node_connected.clear() + time.sleep(15) + else: + # Worker connected but controls not initialized + self.init() + else: + # Node not connected. Wait for reconnect + self.controls_ready = False + self.controls = [] + time.sleep(10) + # Will this nuke the connection? + time.sleep(0.05) + # This is only ran after the main thread is shut down + Logger.log( + LOG_LEVEL["info"], + "{name} Controls Shutting Down...\t\033[1;32m Complete\033[0;0m".format( + **self.config) + ) diff --git a/workers/arduino/arduino_relay_worker.py b/workers/arduino/arduino_relay_worker.py index 35d55cd..0dae496 100644 --- a/workers/arduino/arduino_relay_worker.py +++ b/workers/arduino/arduino_relay_worker.py @@ -9,177 +9,207 @@ from nanpy.serialmanager import SerialManagerError from nanpy.sockconnection import (SocketManager, SocketManagerError) from .worker import Worker -sys.path.append('..') -import variables + + +import constants from logger.Logger import Logger, LOG_LEVEL + class ArduinoRelayWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, relay_available, relay_active, node_connected, connection=None, api=None): - super().__init__(config, main_thread_running, system_ready) - self.config['pin'] = int(self.config['pin']) # parse possbile strings to avoid errors - - if self.config.get('key', None) is None: - raise Exception('No "key" Found in Relay Config') - else: - self.key = self.config.get('key', '').replace(" ", "_").lower() - - if self.config.get('name', None) is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = self.config['name'] - - # Events - self.main_thread_running = main_thread_running - self.system_ready = system_ready - self.relay_available = relay_available - self.relay_active = relay_active - self.node_connected = node_connected - - # Dynamic Properties based on config - self.active = False - self.relay_ready = False - self.topic = self.config['topic'].replace(" ", "/").lower() if self.config['topic'] is not None else 'mudpi/relay/'+self.key - - # Pubsub Listeners - self.pubsub = self.r.pubsub() - self.pubsub.subscribe(**{self.topic: self.handleMessage}) - self.api = api - - if self.node_connected.is_set(): - self.init() - return - - def init(self): - Logger.log(LOG_LEVEL["info"], '{name} Relay Worker {0}...\t\t\033[1;32m Initializing\033[0;0m'.format(self.key)) - self.api = self.api if self.api is not None else ArduinoApi(connection) - self.pin_state_off = self.api.HIGH if self.config['normally_open'] is not None and self.config['normally_open'] else self.api.LOW - self.pin_state_on = self.api.LOW if self.config['normally_open'] is not None and self.config['normally_open'] else self.api.HIGH - self.api.pinMode(self.config['pin'], self.api.OUTPUT) - # Close the relay by default, we use the pin state we determined based on the config at init - self.api.digitalWrite(self.config['pin'], self.pin_state_off) - time.sleep(0.1) - - # Feature to restore relay state in case of crash or unexpected shutdown. This will check for last state stored in redis and set relay accordingly - if(self.config.get('restore_last_known_state', None) is not None and self.config.get('restore_last_known_state', False) is True): - if(self.r.get(self.key+'_state')): - self.api.digitalWrite(self.config['pin'], self.pin_state_on) - Logger.log(LOG_LEVEL["warning"], 'Restoring Relay \033[1;36m{0} On\033[0;0m'.format(self.key)) - - self.relay_ready = True - return - - def run(self): - t = threading.Thread(target=self.work, args=()) - t.start() - Logger.log(LOG_LEVEL["info"], 'Node Relay {0} Worker...\t\t\033[1;32m Online\033[0;0m'.format(self.key)) - return t - - def decodeMessageData(self, message): - if isinstance(message, dict): - #print('Dict Found') - return message - elif isinstance(message.decode('utf-8'), str): - try: - temp = json.loads(message.decode('utf-8')) - #print('Json Found') - return temp - except: - #print('Json Error. Str Found') - return {'event':'Unknown', 'data':message} - else: - #print('Failed to detect type') - return {'event':'Unknown', 'data':message} - - def handleMessage(self, message): - data = message['data'] - if data is not None: - decoded_message = self.decodeMessageData(data) - try: - if decoded_message['event'] == 'Switch': - if decoded_message.get('data', None): - self.relay_active.set() - elif decoded_message.get('data', None) == 0: - self.relay_active.clear() - Logger.log(LOG_LEVEL["info"], 'Switch Relay \033[1;36m{0}\033[0;0m state to \033[1;36m{1}\033[0;0m'.format(self.key, decoded_message['data'])) - elif decoded_message['event'] == 'Toggle': - state = 'Off' if self.active else 'On' - if self.relay_active.is_set(): - self.relay_active.clear() - else: - self.relay_active.set() - Logger.log(LOG_LEVEL["info"], 'Toggle Relay \033[1;36m{0} {1} \033[0;0m'.format(self.key, state)) - except: - Logger.log(LOG_LEVEL["error"], 'Error Decoding Message for Relay {0}'.format(self.key)) - - def elapsedTime(self): - self.time_elapsed = time.perf_counter() - self.time_start - return self.time_elapsed - - def resetElapsedTime(self): - self.time_start = time.perf_counter() - pass - - def turnOn(self): - #Turn on relay if its available - if self.relay_available.is_set(): - if not self.active: - self.api.digitalWrite(self.config['pin'], self.pin_state_on) - message = {'event':'StateChanged', 'data':1} - self.r.set(self.key+'_state', 1) - self.r.publish(self.topic, json.dumps(message)) - self.active = True - #self.relay_active.set() This is handled by the redis listener now - self.resetElapsedTime() - - def turnOff(self): - #Turn off volkeye to flip off relay - if self.relay_available.is_set(): - if self.active: - self.api.digitalWrite(self.config['pin'], self.pin_state_off) - message = {'event':'StateChanged', 'data':0} - self.r.delete(self.key+'_state') - self.r.publish(self.topic, json.dumps(message)) - #self.relay_active.clear() This is handled by the redis listener now - self.active = False - self.resetElapsedTime() - - def work(self): - self.resetElapsedTime() - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - if self.node_connected.is_set(): - if self.relay_ready: - try: - self.pubsub.get_message() - if self.relay_available.is_set(): - if self.relay_active.is_set(): - self.turnOn() - else: - self.turnOff() - else: - self.turnOff() - time.sleep(1) - except e: - Logger.log(LOG_LEVEL["error"], "Node Relay Worker \033[1;36m{0}\033[0;0m \t\033[1;31m Unexpected Error\033[0;0m".format(self.key)) - Logger.log(LOG_LEVEL["error"], "Exception: {0}".format(e)) - else: - self.init() - else: - # Node offline - self.relay_ready = False - time.sleep(5) - - else: - # System not ready relay should be off - self.turnOff() - time.sleep(1) - self.resetElapsedTime() - - time.sleep(0.1) - - - # This is only ran after the main thread is shut down - # Close the pubsub connection - self.pubsub.close() - Logger.log(LOG_LEVEL["info"], "Node Relay {0} Shutting Down...\t\033[1;32m Complete\033[0;0m".format(self.key)) + def __init__(self, config, main_thread_running, system_ready, + relay_available, relay_active, node_connected, + connection=None, api=None): + super().__init__(config, main_thread_running, system_ready) + self.config['pin'] = int( + self.config['pin']) # parse possbile strings to avoid errors + + if self.config.get('key', None) is None: + raise Exception('No "key" Found in Relay Config') + else: + self.key = self.config.get('key', '').replace(" ", "_").lower() + + if self.config.get('name', None) is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = self.config['name'] + + # Events + self.main_thread_running = main_thread_running + self.system_ready = system_ready + self.relay_available = relay_available + self.relay_active = relay_active + self.node_connected = node_connected + + # Dynamic Properties based on config + self.active = False + self.relay_ready = False + self.topic = self.config['topic'].replace(" ", "/").lower() if \ + self.config['topic'] is not None else 'mudpi/relay/' + self.key + + # Pubsub Listeners + self.pubsub = self.r.pubsub() + self.pubsub.subscribe(**{self.topic: self.handle_message}) + self.api = api + + if self.node_connected.is_set(): + self.init() + return + + def init(self): + Logger.log(LOG_LEVEL["info"], + '{name} Relay Worker {0}...\t\t\033[1;32m Initializing\033[0;0m'.format( + self.key)) + self.api = self.api if self.api is not None else ArduinoApi(connection) + self.pin_state_off = self.api.HIGH if self.config[ + 'normally_open'] is not None and \ + self.config[ + 'normally_open'] else self.api.LOW + self.pin_state_on = self.api.LOW if self.config[ + 'normally_open'] is not None and \ + self.config[ + 'normally_open'] else self.api.HIGH + self.api.pinMode(self.config['pin'], self.api.OUTPUT) + # Close the relay by default, we use the pin state we determined based on the config at init + self.api.digitalWrite(self.config['pin'], self.pin_state_off) + time.sleep(0.1) + + # Feature to restore relay state in case of crash or unexpected shutdown. This will check for last state stored in redis and set relay accordingly + if (self.config.get('restore_last_known_state', + None) is not None and self.config.get( + 'restore_last_known_state', False) is True): + if (self.r.get(self.key + '_state')): + self.api.digitalWrite(self.config['pin'], self.pin_state_on) + Logger.log(LOG_LEVEL["warning"], + 'Restoring Relay \033[1;36m{0} On\033[0;0m'.format( + self.key)) + + self.relay_ready = True + return + + def run(self): + t = threading.Thread(target=self.work, args=()) + t.start() + Logger.log(LOG_LEVEL["info"], + 'Node Relay {0} Worker...\t\t\033[1;32m Online\033[0;0m'.format( + self.key)) + return t + + def decode_message_data(self, message): + if isinstance(message, dict): + # print('Dict Found') + return message + elif isinstance(message.decode('utf-8'), str): + try: + temp = json.loads(message.decode('utf-8')) + # print('Json Found') + return temp + except: + # print('Json Error. Str Found') + return {'event': 'Unknown', 'data': message} + else: + # print('Failed to detect type') + return {'event': 'Unknown', 'data': message} + + def handle_message(self, message): + data = message['data'] + if data is not None: + decoded_message = self.decode_message_data(data) + try: + if decoded_message['event'] == 'Switch': + if decoded_message.get('data', None): + self.relay_active.set() + elif decoded_message.get('data', None) == 0: + self.relay_active.clear() + Logger.log(LOG_LEVEL["info"], + 'Switch Relay \033[1;36m{0}\033[0;0m state to \033[1;36m{1}\033[0;0m'.format( + self.key, decoded_message['data'])) + elif decoded_message['event'] == 'Toggle': + state = 'Off' if self.active else 'On' + if self.relay_active.is_set(): + self.relay_active.clear() + else: + self.relay_active.set() + Logger.log(LOG_LEVEL["info"], + 'Toggle Relay \033[1;36m{0} {1} \033[0;0m'.format( + self.key, state)) + except: + Logger.log(LOG_LEVEL["error"], + 'Error Decoding Message for Relay {0}'.format( + self.key)) + + def elapsed_time(self): + self.time_elapsed = time.perf_counter() - self.time_start + return self.time_elapsed + + def reset_elapsed_time(self): + self.time_start = time.perf_counter() + pass + + def turn_on(self): + # Turn on relay if its available + if self.relay_available.is_set(): + if not self.active: + self.api.digitalWrite(self.config['pin'], self.pin_state_on) + message = {'event': 'StateChanged', 'data': 1} + self.r.set(self.key + '_state', 1) + self.r.publish(self.topic, json.dumps(message)) + self.active = True + # self.relay_active.set() This is handled by the redis listener now + self.reset_elapsed_time() + + def turn_off(self): + # Turn off volkeye to flip off relay + if self.relay_available.is_set(): + if self.active: + self.api.digitalWrite(self.config['pin'], self.pin_state_off) + message = {'event': 'StateChanged', 'data': 0} + self.r.delete(self.key + '_state') + self.r.publish(self.topic, json.dumps(message)) + # self.relay_active.clear() This is handled by the redis listener now + self.active = False + self.reset_elapsed_time() + + def work(self): + self.reset_elapsed_time() + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + if self.node_connected.is_set(): + if self.relay_ready: + try: + self.pubsub.get_message() + if self.relay_available.is_set(): + if self.relay_active.is_set(): + self.turn_on() + else: + self.turn_off() + else: + self.turn_off() + time.sleep(1) + except e: + Logger.log(LOG_LEVEL["error"], + "Node Relay Worker \033[1;36m{0}\033[0;0m \t\033[1;31m Unexpected Error\033[0;0m".format( + self.key)) + Logger.log(LOG_LEVEL["error"], + "Exception: {0}".format(e)) + else: + self.init() + else: + # Node offline + self.relay_ready = False + time.sleep(5) + + else: + # System not ready relay should be off + self.turn_off() + time.sleep(1) + self.reset_elapsed_time() + + time.sleep(0.1) + + # This is only ran after the main thread is shut down + # Close the pubsub connection + self.pubsub.close() + Logger.log(LOG_LEVEL["info"], + "Node Relay {0} Shutting Down...\t\033[1;32m Complete\033[0;0m".format( + self.key)) diff --git a/workers/arduino/arduino_sensor_worker.py b/workers/arduino/arduino_sensor_worker.py index d56d70d..dead8c3 100644 --- a/workers/arduino/arduino_sensor_worker.py +++ b/workers/arduino/arduino_sensor_worker.py @@ -14,106 +14,123 @@ from sensors.arduino.humidity_sensor import (HumiditySensor) from sensors.arduino.temperature_sensor import (TemperatureSensor) import sys -sys.path.append('..') + + import importlib from logger.Logger import Logger, LOG_LEVEL -#r = redis.Redis(host='127.0.0.1', port=6379) + +# r = redis.Redis(host='127.0.0.1', port=6379) class ArduinoSensorWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, node_connected, connection=None, api=None): - super().__init__(config, main_thread_running, system_ready) - self.topic = config.get('topic', 'sensors').replace(" ", "_").lower() - self.sensors_ready = False - self.node_connected = node_connected - self.connection = connection - self.api = api - self.sensors = [] - if node_connected.is_set(): - self.init() - self.sensors_ready = True - return - - def init(self, connection=None): - Logger.log(LOG_LEVEL["info"], '{name} Sensor Worker...\t\t\033[1;32m Preparing\033[0;0m'.format(**self.config)) - try: - for sensor in self.config['sensors']: - if sensor.get('type', None) is not None: - #Get the sensor from the sensors folder {sensor name}_sensor.{SensorName}Sensor - sensor_type = 'sensors.arduino.' + sensor.get('type').lower() + '_sensor.' + sensor.get('type').capitalize() + 'Sensor' - - #analog_pin_mode = False if sensor.get('is_digital', False) else True - - imported_sensor = self.dynamic_import(sensor_type) - #new_sensor = imported_sensor(sensor.get('pin'), name=sensor.get('name', sensor.get('type')), connection=self.connection, key=sensor.get('key', None)) - - # Define default kwargs for all sensor types, conditionally include optional variables below if they exist - sensor_kwargs = { - 'name' : sensor.get('name', None), - 'pin' : int(sensor.get('pin')), - 'connection': self.connection, - 'key' : sensor.get('key', None) - } - - # optional sensor variables - # Model is specific to DHT modules to specify DHT11(11) DHT22(22) or DHT2301(21) - if sensor.get('model'): - sensor_kwargs['model'] = str(sensor.get('model')) - sensor_kwargs['api'] = self.api - - new_sensor = imported_sensor(**sensor_kwargs) - - # print('{type} Sensor {pin}...\t\t\t\033[1;32m Preparing\033[0;0m'.format(**sensor)) - new_sensor.init_sensor() - self.sensors.append(new_sensor) - # print('{type} Sensor {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format(**sensor)) - self.sensors_ready = True - except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: - # Connection error. Reset everything for reconnect - self.sensors_ready = False - self.node_connected.clear() - self.sensors = [] - return - - def run(self): - t = threading.Thread(target=self.work, args=()) - t.start() - Logger.log(LOG_LEVEL["info"], 'Node {name} Sensor Worker...\t\t\033[1;32m Online\033[0;0m'.format(**self.config)) - return t - - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - if self.node_connected.is_set(): - if self.sensors_ready: - try: - message = {'event':'SensorUpdate'} - readings = {} - for sensor in self.sensors: - result = sensor.read() - readings[sensor.key] = result - #r.set(sensor.get('key', sensor.get('type')), value) - - Logger.log(LOG_LEVEL["debug"], "Node Readings: ", readings) - message['data'] = readings - self.r.publish(self.topic, json.dumps(message)) - except (SerialManagerError, SocketManagerError, BrokenPipeError, ConnectionResetError, OSError, socket.timeout) as e: - Logger.log(LOG_LEVEL["warning"], '\033[1;36m{name}\033[0;0m -> \033[1;33mSensors Timeout!\033[0;0m'.format(**self.config)) - self.sensors = [] - self.node_connected.clear() - time.sleep(15) - else: - # Worker connected but sensors not initialized - self.init() - self.sensors_ready = True - else: - #Node not connected, sensors not ready. Wait for reconnect - self.sensors = [] - self.sensors_ready = False - - # Main loop delay between cycles - time.sleep(self.sleep_duration) - - #This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "{name} Sensors Shutting Down...\t\033[1;32m Complete\033[0;0m".format(**self.config)) \ No newline at end of file + def __init__(self, config, main_thread_running, system_ready, + node_connected, connection=None, api=None): + super().__init__(config, main_thread_running, system_ready) + self.topic = config.get('topic', 'sensors').replace(" ", "_").lower() + self.sensors_ready = False + self.node_connected = node_connected + self.connection = connection + self.api = api + self.sensors = [] + if node_connected.is_set(): + self.init() + self.sensors_ready = True + return + + def init(self, connection=None): + Logger.log(LOG_LEVEL["info"], + '{name} Sensor Worker...\t\t\033[1;32m Preparing\033[0;0m'.format( + **self.config)) + try: + for sensor in self.config['sensors']: + if sensor.get('type', None) is not None: + # Get the sensor from the sensors folder {sensor name}_sensor.{SensorName}Sensor + sensor_type = 'sensors.arduino.' + sensor.get( + 'type').lower() + '_sensor.' + sensor.get( + 'type').capitalize() + 'Sensor' + + # analog_pin_mode = False if sensor.get('is_digital', False) else True + + imported_sensor = self.dynamic_import(sensor_type) + # new_sensor = imported_sensor(sensor.get('pin'), name=sensor.get('name', sensor.get('type')), connection=self.connection, key=sensor.get('key', None)) + + # Define default kwargs for all sensor types, conditionally include optional variables below if they exist + sensor_kwargs = { + 'name': sensor.get('name', None), + 'pin': int(sensor.get('pin')), + 'connection': self.connection, + 'key': sensor.get('key', None) + } + + # optional sensor variables + # Model is specific to DHT modules to specify DHT11(11) DHT22(22) or DHT2301(21) + if sensor.get('model'): + sensor_kwargs['model'] = str(sensor.get('model')) + sensor_kwargs['api'] = self.api + + new_sensor = imported_sensor(**sensor_kwargs) + + # print('{type} Sensor {pin}...\t\t\t\033[1;32m Preparing\033[0;0m'.format(**sensor)) + new_sensor.init_sensor() + self.sensors.append(new_sensor) + # print('{type} Sensor {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format(**sensor)) + self.sensors_ready = True + except (SerialManagerError, SocketManagerError, BrokenPipeError, + ConnectionResetError, OSError, socket.timeout) as e: + # Connection error. Reset everything for reconnect + self.sensors_ready = False + self.node_connected.clear() + self.sensors = [] + return + + def run(self): + t = threading.Thread(target=self.work, args=()) + t.start() + Logger.log(LOG_LEVEL["info"], + 'Node {name} Sensor Worker...\t\t\033[1;32m Online\033[0;0m'.format( + **self.config)) + return t + + def work(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + if self.node_connected.is_set(): + if self.sensors_ready: + try: + message = {'event': 'SensorUpdate'} + readings = {} + for sensor in self.sensors: + result = sensor.read() + readings[sensor.key] = result + # r.set(sensor.get('key', sensor.get('type')), value) + + Logger.log(LOG_LEVEL["debug"], + "Node Readings: {0}".format(readings)) + message['data'] = readings + self.r.publish(self.topic, json.dumps(message)) + except (SerialManagerError, SocketManagerError, + BrokenPipeError, ConnectionResetError, OSError, + socket.timeout) as e: + Logger.log(LOG_LEVEL["warning"], + '\033[1;36m{name}\033[0;0m -> \033[1;33mSensors Timeout!\033[0;0m'.format( + **self.config)) + self.sensors = [] + self.node_connected.clear() + time.sleep(15) + else: + # Worker connected but sensors not initialized + self.init() + self.sensors_ready = True + else: + # Node not connected, sensors not ready. Wait for reconnect + self.sensors = [] + self.sensors_ready = False + + # Main loop delay between cycles + time.sleep(self.sleep_duration) + + # This is only ran after the main thread is shut down + Logger.log(LOG_LEVEL["info"], + "{name} Sensors Shutting Down...\t\033[1;32m Complete\033[0;0m".format( + **self.config)) diff --git a/workers/arduino/arduino_worker.py b/workers/arduino/arduino_worker.py index 0e08445..7b193d3 100644 --- a/workers/arduino/arduino_worker.py +++ b/workers/arduino/arduino_worker.py @@ -11,177 +11,234 @@ from workers.arduino.arduino_relay_worker import ArduinoRelayWorker from .worker import Worker import sys -sys.path.append('..') + + import importlib from logger.Logger import Logger, LOG_LEVEL -#r = redis.Redis(host='127.0.0.1', port=6379) + +# r = redis.Redis(host='127.0.0.1', port=6379) class ArduinoWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, connection=None): - super().__init__(config, main_thread_running, system_ready) - self.connection = connection - self.threads = [] - - # Events - self.node_ready = threading.Event() - self.node_connected = threading.Event() # Event to signal if node can be used - - self.workers = [] - self.relays = [] - self.relayEvents = {} - self.relay_index = 0 - - if connection is None: - self.connection = self.connect() - - try: - if self.config['controls'] is not None: - acw = ArduinoControlWorker(self.config, main_thread_running, system_ready, self.node_connected, self.connection) - self.workers.append(acw) - time.sleep(3) - except KeyError: - Logger.log(LOG_LEVEL["info"], '{name} Node Controls...\t\t\033[1;31m Disabled\033[0;0m'.format(**self.config)) - - try: - if self.config['relays'] is not None: - for relay in self.config['relays']: - #Create a threading event for each relay to check status - relayState = { - "available": threading.Event(), #Event to allow relay to activate - "active": threading.Event() #Event to signal relay to open/close - } - # Store the relays under the key or index if no key is found, this way we can reference the right relays - self.relayEvents[relay.get("key", self.relay_index)] = relayState - # Create sensor worker for a relay - arw = ArduinoRelayWorker(relay, main_thread_running, system_ready, relayState['available'], relayState['active'], self.node_connected, self.connection, self.api) - # Make the relays available, this event is toggled off elsewhere if we need to disable relays - relayState['available'].set() - self.relay_index +=1 - self.workers.append(arw) - time.sleep(3) - except KeyError: - Logger.log(LOG_LEVEL["info"], '{name} Node Relays...\t\t\033[1;31m Disabled\033[0;0m'.format(**self.config)) - - try: - if self.config['sensors'] is not None: - asw = ArduinoSensorWorker(self.config, main_thread_running, system_ready, self.node_connected, self.connection, self.api) - self.workers.append(asw) - time.sleep(3) - except KeyError: - Logger.log(LOG_LEVEL["info"], '{name} Node Sensors...\t\t\033[1;31m Disabled\033[0;0m'.format(**self.config)) - - return - - def connect(self): - attempts = 3 - conn = None - if self.config.get('use_wifi', False): - while attempts > 0 and self.main_thread_running.is_set(): - try: - Logger.log(LOG_LEVEL["debug"], '\033[1;36m{0}\033[0;0m -> Connecting... \t'.format(self.config["name"], (3-attempts))) - attempts-= 1 - conn = SocketManager(host=str(self.config.get('address', 'mudpi.local'))) - # Test the connection with api - self.api = ArduinoApi(connection=conn) - except (SocketManagerError, BrokenPipeError, ConnectionResetError, socket.timeout) as e: - Logger.log(LOG_LEVEL["warning"], '{name} -> Connecting...\t\t\033[1;33m Timeout\033[0;0m '.format(**self.config)) - if attempts > 0: - Logger.log(LOG_LEVEL["info"], '{name} -> Preparing Reconnect... \t'.format(**self.config)) - else: - Logger.log(LOG_LEVEL["error"], '{name} -> Connection Attempts...\t\033[1;31m Failed\033[0;0m '.format(**self.config)) - conn = None - self.resetConnection() - time.sleep(15) - except (OSError, KeyError) as e: - Logger.log(LOG_LEVEL["error"], '[{name}] \033[1;33m Node Not Found. (Is it online?)\033[0;0m'.format(**self.config)) - conn = None - self.resetConnection() - time.sleep(15) - else: - Logger.log(LOG_LEVEL["info"], '{name} -> Wifi Connection \t\t\033[1;32m Success\033[0;0m '.format(**self.config)) - for worker in self.workers: - worker.connection = conn - self.node_connected.set() - self.node_ready.set() - break - else: - while attempts > 0 and self.main_thread_running.is_set(): - try: - attempts-= 1 - conn = SerialManager(device=str(self.config.get('address', '/dev/ttyUSB1'))) - except SerialManagerError: - Logger.log(LOG_LEVEL["warning"], '{name} -> Connecting...\t\t\033[1;33m Timeout\033[0;0m '.format(**self.config)) - if attempts > 0: - Logger.log(LOG_LEVEL["info"], '{name} -> Preparing Reconnect... \t'.format(**self.config), end='\r', flush=True) - else: - Logger.log(LOG_LEVEL["error"], '{name} -> Connection Attempts...\t\033[1;31m Failed\033[0;0m '.format(**self.config)) - self.resetConnection() - conn = None - time.sleep(15) - else: - if conn is not None: - Logger.log(LOG_LEVEL["info"], '[{name}] Serial Connection \t\033[1;32m Success\033[0;0m '.format(**self.config)) - for worker in self.workers: - worker.connection = conn - self.node_connected.set() - self.node_ready.set() - break - return conn - - def resetConnection(self): - self.connection = None - self.node_connected.clear() - self.node_ready.clear() - - - def run(self): - for worker in self.workers: - t = worker.run() - self.threads.append(t) - time.sleep(1) - - t = threading.Thread(target=self.work, args=()) - t.start() - if self.node_ready.is_set(): - Logger.log(LOG_LEVEL["info"], str(self.config['name']) +' Node Worker '+ '[S: ' + str(len(self.config.get('sensors', []))) + ']' + '[C: ' + str(len(self.config.get('controls', []))) + ']...\t\033[1;32m Online\033[0;0m') - else: - Logger.log(LOG_LEVEL["info"], str(self.config['name']) +'...\t\t\t\t\033[1;33m Pending Reconnect\033[0;0m ') - return t - - def work(self): - delay_multiplier = 1 - while self.main_thread_running.is_set(): - if self.system_ready.is_set() and self.node_ready.is_set(): - if not self.node_connected.is_set(): - #Connection Broken - Reset Connection - self.resetConnection() - Logger.log(LOG_LEVEL["warning"], '\033[1;36m{name}\033[0;0m -> \033[1;33mTimeout!\033[0;0m \t\t\t\033[1;31m Connection Broken\033[0;0m'.format(**self.config)) - time.sleep(30) - else: - # Node reconnection cycle - if not self.node_connected.is_set(): - # Random delay before connections to offset multiple attempts (1-5 min delay) - random_delay = (random.randrange(30, self.config.get("max_reconnect_delay", 300)) * delay_multiplier) / 2 - time.sleep(10) - Logger.log(LOG_LEVEL["info"], '\033[1;36m'+str(self.config['name']) +'\033[0;0m -> Retrying in '+ '{0}s...'.format(random_delay)+'\t\033[1;33m Pending Reconnect\033[0;0m ') - # Two separate checks for main thread event to prevent re-connections during shutdown - if self.main_thread_running.is_set(): - time.sleep(random_delay) - if self.main_thread_running.is_set(): - self.connection = self.connect() - if self.connection is None: - delay_multiplier += 1 - if delay_multiplier > 6: - delay_multiplier = 6 - else: - delay_multiplier = 1 - # Main loop delay between cycles - time.sleep(self.sleep_duration) - - # This is only ran after the main thread is shut down - # Join all our sub threads for shutdown - for thread in self.threads: - thread.join() - Logger.log(LOG_LEVEL["info"], ("{name} Shutting Down...\t\t\033[1;32m Complete\033[0;0m".format(**self.config))) \ No newline at end of file + def __init__(self, config, main_thread_running, system_ready, + connection=None): + super().__init__(config, main_thread_running, system_ready) + self.connection = connection + self.threads = [] + + # Events + self.node_ready = threading.Event() + self.node_connected = threading.Event() # Event to signal if node can be used + + self.workers = [] + self.relays = [] + self.relayEvents = {} + self.relay_index = 0 + + if connection is None: + self.connection = self.connect() + + try: + if self.config['controls'] is not None: + acw = ArduinoControlWorker(self.config, main_thread_running, + system_ready, self.node_connected, + self.connection) + self.workers.append(acw) + time.sleep(3) + except KeyError: + Logger.log(LOG_LEVEL["info"], + '{name} Node Controls...\t\t\033[1;31m Disabled\033[0;0m'.format( + **self.config)) + + try: + if self.config['relays'] is not None: + for relay in self.config['relays']: + # Create a threading event for each relay to check status + relayState = { + "available": threading.Event(), + # Event to allow relay to activate + "active": threading.Event() + # Event to signal relay to open/close + } + # Store the relays under the key or index if no key is found, this way we can reference the right relays + self.relayEvents[ + relay.get("key", self.relay_index)] = relayState + # Create sensor worker for a relay + arw = ArduinoRelayWorker(relay, main_thread_running, + system_ready, + relayState['available'], + relayState['active'], + self.node_connected, + self.connection, self.api) + # Make the relays available, this event is toggled off elsewhere if we need to disable relays + relayState['available'].set() + self.relay_index += 1 + self.workers.append(arw) + time.sleep(3) + except KeyError: + Logger.log(LOG_LEVEL["info"], + '{name} Node Relays...\t\t\033[1;31m Disabled\033[0;0m'.format( + **self.config)) + + try: + if self.config['sensors'] is not None: + asw = ArduinoSensorWorker(self.config, main_thread_running, + system_ready, self.node_connected, + self.connection, self.api) + self.workers.append(asw) + time.sleep(3) + except KeyError: + Logger.log(LOG_LEVEL["info"], + '{name} Node Sensors...\t\t\033[1;31m Disabled\033[0;0m'.format( + **self.config)) + + return + + def connect(self): + attempts = 3 + conn = None + if self.config.get('use_wifi', False): + while attempts > 0 and self.main_thread_running.is_set(): + try: + Logger.log(LOG_LEVEL["debug"], + '\033[1;36m{0}\033[0;0m -> Connecting... \t'.format( + self.config["name"], (3 - attempts))) + attempts -= 1 + conn = SocketManager( + host=str(self.config.get('address', 'mudpi.local'))) + # Test the connection with api + self.api = ArduinoApi(connection=conn) + except ( + SocketManagerError, BrokenPipeError, ConnectionResetError, + socket.timeout) as e: + Logger.log(LOG_LEVEL["warning"], + '{name} -> Connecting...\t\t\033[1;33m Timeout\033[0;0m '.format( + **self.config)) + if attempts > 0: + Logger.log(LOG_LEVEL["info"], + '{name} -> Preparing Reconnect... \t'.format( + **self.config)) + else: + Logger.log(LOG_LEVEL["error"], + '{name} -> Connection Attempts...\t\033[1;31m Failed\033[0;0m '.format( + **self.config)) + conn = None + self.resetConnection() + time.sleep(15) + except (OSError, KeyError) as e: + Logger.log(LOG_LEVEL["error"], + '[{name}] \033[1;33m Node Not Found. (Is it online?)\033[0;0m'.format( + **self.config)) + conn = None + self.resetConnection() + time.sleep(15) + else: + Logger.log(LOG_LEVEL["info"], + '{name} -> Wifi Connection \t\t\033[1;32m Success\033[0;0m '.format( + **self.config)) + for worker in self.workers: + worker.connection = conn + self.node_connected.set() + self.node_ready.set() + break + else: + while attempts > 0 and self.main_thread_running.is_set(): + try: + attempts -= 1 + conn = SerialManager( + device=str(self.config.get('address', '/dev/ttyUSB1'))) + except SerialManagerError: + Logger.log(LOG_LEVEL["warning"], + '{name} -> Connecting...\t\t\033[1;33m Timeout\033[0;0m '.format( + **self.config)) + if attempts > 0: + Logger.log(LOG_LEVEL["info"], + '{name} -> Preparing Reconnect... \t'.format( + **self.config), end='\r', flush=True) + else: + Logger.log(LOG_LEVEL["error"], + '{name} -> Connection Attempts...\t\033[1;31m Failed\033[0;0m '.format( + **self.config)) + self.resetConnection() + conn = None + time.sleep(15) + else: + if conn is not None: + Logger.log(LOG_LEVEL["info"], + '[{name}] Serial Connection \t\033[1;32m Success\033[0;0m '.format( + **self.config)) + for worker in self.workers: + worker.connection = conn + self.node_connected.set() + self.node_ready.set() + break + return conn + + def resetConnection(self): + self.connection = None + self.node_connected.clear() + self.node_ready.clear() + + def run(self): + for worker in self.workers: + t = worker.run() + self.threads.append(t) + time.sleep(1) + + t = threading.Thread(target=self.work, args=()) + t.start() + if self.node_ready.is_set(): + Logger.log(LOG_LEVEL["info"], str( + self.config['name']) + ' Node Worker ' + '[S: ' + str( + len(self.config.get('sensors', []))) + ']' + '[C: ' + str(len( + self.config.get('controls', + []))) + ']...\t\033[1;32m Online\033[0;0m') + else: + Logger.log(LOG_LEVEL["info"], str(self.config[ + 'name']) + '...\t\t\t\t\033[1;33m Pending Reconnect\033[0;0m ') + return t + + def work(self): + delay_multiplier = 1 + while self.main_thread_running.is_set(): + if self.system_ready.is_set() and self.node_ready.is_set(): + if not self.node_connected.is_set(): + # Connection Broken - Reset Connection + self.resetConnection() + Logger.log(LOG_LEVEL["warning"], + '\033[1;36m{name}\033[0;0m -> \033[1;33mTimeout!\033[0;0m \t\t\t\033[1;31m Connection Broken\033[0;0m'.format( + **self.config)) + time.sleep(30) + else: + # Node reconnection cycle + if not self.node_connected.is_set(): + # Random delay before connections to offset multiple attempts (1-5 min delay) + random_delay = (random.randrange(30, self.config.get( + "max_reconnect_delay", 300)) * delay_multiplier) / 2 + time.sleep(10) + Logger.log(LOG_LEVEL["info"], '\033[1;36m' + str( + self.config[ + 'name']) + '\033[0;0m -> Retrying in ' + '{0}s...'.format( + random_delay) + '\t\033[1;33m Pending Reconnect\033[0;0m ') + # Two separate checks for main thread event to prevent re-connections during shutdown + if self.main_thread_running.is_set(): + time.sleep(random_delay) + if self.main_thread_running.is_set(): + self.connection = self.connect() + if self.connection is None: + delay_multiplier += 1 + if delay_multiplier > 6: + delay_multiplier = 6 + else: + delay_multiplier = 1 + # Main loop delay between cycles + time.sleep(self.sleep_duration) + + # This is only ran after the main thread is shut down + # Join all our sub threads for shutdown + for thread in self.threads: + thread.join() + Logger.log(LOG_LEVEL["info"], ( + "{name} Shutting Down...\t\t\033[1;32m Complete\033[0;0m".format( + **self.config))) diff --git a/workers/arduino/worker.py b/workers/arduino/worker.py index b0796e6..9d0125e 100644 --- a/workers/arduino/worker.py +++ b/workers/arduino/worker.py @@ -4,69 +4,72 @@ import redis import threading import sys -sys.path.append('..') + + from logger.Logger import Logger, LOG_LEVEL + # Base Worker Class # A worker is responsible for handling its set of operations and running on a thread class Worker(): - def __init__(self, config, main_thread_running, system_ready): - self.config = config - try: - self.r = config["redis"] - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - self.topic = config.get('topic', 'mudpi').replace(" ", "_").lower() - self.sleep_duration = config.get('sleep_duration', 15) + def __init__(self, config, main_thread_running, system_ready): + self.config = config + try: + self.r = config["redis"] + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + self.topic = config.get('topic', 'mudpi').replace(" ", "_").lower() + self.sleep_duration = config.get('sleep_duration', 15) - # Threading Events to Keep Everything in Sync - self.main_thread_running = main_thread_running - self.system_ready = system_ready - self.worker_available = threading.Event() + # Threading Events to Keep Everything in Sync + self.main_thread_running = main_thread_running + self.system_ready = system_ready + self.worker_available = threading.Event() - self.api = None - self.components = [] - return + self.api = None + self.components = [] + return - def init(self): - # print('Worker...\t\t\t\033[1;32m Initializing\033[0;0m'.format(**control)) - return + def init(self): + # print('Worker...\t\t\t\033[1;32m Initializing\033[0;0m'.format(**control)) + return - def run(self): - t = threading.Thread(target=self.work, args=()) - t.start() - return t + def run(self): + t = threading.Thread(target=self.work, args=()) + t.start() + return t - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - time.sleep(self.sleep_duration) - #This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") + def work(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + time.sleep(self.sleep_duration) + # This is only ran after the main thread is shut down + Logger.log(LOG_LEVEL["info"], + "Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") - def dynamic_import(self, name): - # Split path of the class folder structure: {sensor name}_sensor . {SensorName}Sensor - components = name.split('.') - # Dynamically import root of component path - module = __import__(components[0]) - # Get component attributes - for component in components[1:]: - module = getattr(module, component) - return module + def dynamic_import(self, name): + # Split path of the class folder structure: {sensor name}_sensor . {SensorName}Sensor + components = name.split('.') + # Dynamically import root of component path + module = __import__(components[0]) + # Get component attributes + for component in components[1:]: + module = getattr(module, component) + return module - def decodeMessageData(self, message): - if isinstance(message, dict): - #print('Dict Found') - return message - elif isinstance(message.decode('utf-8'), str): - try: - temp = json.loads(message.decode('utf-8')) - #print('Json Found') - return temp - except: - #print('Json Error. Str Found') - return {'event':'Unknown', 'data':message} - else: - #print('Failed to detect type') - return {'event':'Unknown', 'data':message} \ No newline at end of file + def decode_message_data(self, message): + if isinstance(message, dict): + # print('Dict Found') + return message + elif isinstance(message.decode('utf-8'), str): + try: + temp = json.loads(message.decode('utf-8')) + # print('Json Found') + return temp + except: + # print('Json Error. Str Found') + return {'event': 'Unknown', 'data': message} + else: + # print('Failed to detect type') + return {'event': 'Unknown', 'data': message} diff --git a/workers/pi/__init__.py b/workers/linux/__init__.py similarity index 100% rename from workers/pi/__init__.py rename to workers/linux/__init__.py diff --git a/workers/linux/camera_worker.py b/workers/linux/camera_worker.py new file mode 100644 index 0000000..04ce4cd --- /dev/null +++ b/workers/linux/camera_worker.py @@ -0,0 +1,208 @@ +""" +This is (for instance) a Raspberry Pi only worker! + + +The libcamera project (in development), aims to offer an open source stack for +cameras for Linux, ChromeOS and Android. +It will be able to detect and manage all of the exposed camera on the system. +Connected via USB or CSI (Rasperry pi camera). +libcamera developers plan to privide Python bindings: +https://www.raspberrypi.org/blog/an-open-source-camera-stack-for-raspberry-pi-using-libcamera/#comment-1528789 + +Not available at time of writing: 9 Nov 2020 + +Once available, we should look forward migrating to this library, as it would +allow our worker to support multiple boards and devices. +""" + +import datetime +import json +import os +import sys +import threading +import time + +from picamera import PiCamera +from utils import get_config_item + +from workers.linux.worker import Worker + + + +from logger.Logger import Logger, LOG_LEVEL + + +class CameraWorker(Worker): + def __init__(self, config, main_thread_running, system_ready, + camera_available): + super().__init__(config, main_thread_running, system_ready) + self.pending_reset = False + + # Events + self.camera_available = camera_available + + # Dynamic Properties based on config + self.path = get_config_item(self.config, 'path', '/etc/mudpi/img/') + self.topic = get_config_item( + self.config, 'topic', 'mudpi/camera/', replace_char="/" + ) + + if self.config['resolution'] is not None: + self.resolutionX = int(self.config['resolution'].get('x', 1920)) + self.resolutionY = int(self.config['resolution'].get('y', 1080)) + if self.config['delay'] is not None: + self.hours = int(self.config['delay'].get('hours', 0)) + self.minutes = int(self.config['delay'].get('minutes', 0)) + self.seconds = int(self.config['delay'].get('seconds', 0)) + + self.init() + return + + def init(self): + try: + self.camera = PiCamera( + resolution=(self.resolutionX, self.resolutionY)) + # Below we calibrate the camera for consistent imaging + self.camera.framerate = 30 + # Wait for the automatic gain control to settle + time.sleep(2) + # Now fix the values + self.camera.shutter_speed = self.camera.exposure_speed + self.camera.exposure_mode = 'off' + g = self.camera.awb_gains + self.camera.awb_mode = 'off' + self.camera.awb_gains = g + except Exception: + self.camera = PiCamera() + + # Pubsub Listeners + self.pubsub = self.r.pubsub() + self.pubsub.subscribe(**{self.topic: self.handle_event}) + + Logger.log( + LOG_LEVEL["info"], + 'Camera Worker...\t\t\t\033[1;32m Ready\033[0;0m' + ) + return + + def run(self): + thread = threading.Thread(target=self.work, args=()) + thread.start() + self.listener = threading.Thread(target=self.listen, args=()) + self.listener.start() + Logger.log( + LOG_LEVEL["info"], + 'Camera Worker...\t\t\t\033[1;32m Online\033[0;0m' + ) + return thread + + def wait(self): + # Calculate the delay + try: + self.next_time = (datetime.datetime.now() + datetime.timedelta( + hours=self.hours, minutes=self.minutes, + seconds=self.seconds)).replace(microsecond=0) + except Exception: + # Default every hour + self.next_time = ( + datetime.datetime.now() + datetime.timedelta(hours=1) + ).replace(minute=0, second=0, microsecond=0) + delay = (self.next_time - datetime.datetime.now()).seconds + time.sleep(delay) + + def handle_event(self, message): + data = message['data'] + decoded_message = None + + if data is not None: + + try: + if isinstance(data, dict): + decoded_message = data + + elif isinstance(data.decode('utf-8'), str): + temp = json.loads(data.decode('utf-8')) + decoded_message = temp + if decoded_message['event'] == 'Timelapse': + Logger.log( + LOG_LEVEL["info"], + "Camera Signaled for Reset" + ) + self.camera_available.clear() + self.pending_reset = True + except Exception: + Logger.log(LOG_LEVEL["error"], + 'Error Handling Event for Camera') + + def listen(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + if self.camera_available.is_set(): + self.pubsub.get_message() + time.sleep(1) + else: + delay = ( + self.next_time - datetime.datetime.now() + ).seconds + 15 + # wait 15 seconds after next scheduled picture + time.sleep(delay) + self.camera_available.set() + else: + time.sleep(2) + return + + def work(self): + self.reset_elapsed_time() + + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + + if self.camera_available.is_set(): + # try: + for i, filename in enumerate( + self.camera.capture_continuous( + self.path + 'mudpi-{counter:05d}.jpg')): + + if not self.camera_available.is_set(): + if self.pending_reset: + try: + # cleanup previous file + os.remove( + filename + ) + self.pending_reset = False + except Exception: + Logger.log( + LOG_LEVEL["error"], + "Error During Camera Reset Cleanup" + ) + break + message = {'event': 'StateChanged', 'data': filename} + self.r.set('last_camera_image', filename) + self.r.publish(self.topic, json.dumps(message)) + Logger.log( + LOG_LEVEL["debug"], + 'Image Captured \033[1;36m%s\033[0;0m' % filename + ) + self.wait() + # except: + # print("Camera Worker \t\033[1;31m Unexpected Error\033[0;0m") + # time.sleep(30) + else: + time.sleep(1) + self.reset_elapsed_time() + else: + # System not ready camera should be off + time.sleep(1) + self.reset_elapsed_time() + + time.sleep(0.1) + + # This is only ran after the main thread is shut down + self.camera.close() + self.listener.join() + self.pubsub.close() + Logger.log( + LOG_LEVEL["info"], + "Camera Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m" + ) diff --git a/workers/linux/control_worker.py b/workers/linux/control_worker.py new file mode 100644 index 0000000..adcabbe --- /dev/null +++ b/workers/linux/control_worker.py @@ -0,0 +1,81 @@ +import sys +import time + +from utils import get_config_item +from workers.linux.worker import Worker + + + +from logger.Logger import Logger, LOG_LEVEL + + +class PiControlWorker(Worker): + def __init__(self, config, main_thread_running, system_ready): + super().__init__(config, main_thread_running, system_ready) + self.topic = get_config_item(self.config, 'topic', 'controls') + self.sleep_duration = config.get('sleep_duration', 0.5) + + self.controls = [] + self.init() + return + + def init(self): + for control in self.config['controls']: + if control.get('type', None) is not None: + # Get the control from the controls folder + # {control name}_control.{ControlName}Control + control_type = 'controls.linux.' + control_type += control.get('type').lower() + control_type += '_control.' + control_type += control.get('type').capitalize() + 'Control' + + imported_control = self.dynamic_import(control_type) + + # Define default kwargs for all control types, + # conditionally include optional variables below if they exist + control_kwargs = { + 'name': control.get('name', None), + 'pin': int(control.get('pin')), + 'key': control.get('key', None), + 'topic': control.get('topic', None), + 'resistor': control.get('resistor', None), + 'edge_detection': control.get('edge_detection', None), + 'debounce': control.get('debounce', None) + } + + # optional control variables + # add conditional control vars here... + + new_control = imported_control(**control_kwargs) + + new_control.init_control() + self.controls.append(new_control) + Logger.log( + LOG_LEVEL["info"], + '{type} Control {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format( + **control) + ) + return + + def run(self): + Logger.log( + LOG_LEVEL["info"], + 'Pi Control Worker [' + str( + len(self.config['controls']) + ) + ' Controls]...\t\033[1;32m Online\033[0;0m' + ) + return super().run() + + def work(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + readings = {} + for control in self.controls: + result = control.read() + readings[control.key] = result + time.sleep(self.sleep_duration) + # This is only ran after the main thread is shut down + Logger.log( + LOG_LEVEL["info"], + "Pi Control Worker Shutting Down...\t\033[1;32m Complete\033[0;0m" + ) diff --git a/workers/linux/i2c_worker.py b/workers/linux/i2c_worker.py new file mode 100644 index 0000000..747c76c --- /dev/null +++ b/workers/linux/i2c_worker.py @@ -0,0 +1,87 @@ +import json +import sys +import time + + +from .worker import Worker + +from logger.Logger import Logger, LOG_LEVEL + + +class PiI2CWorker(Worker): + def __init__(self, config, main_thread_running, system_ready): + super().__init__(config, main_thread_running, system_ready) + self.topic = config.get('topic', 'i2c').replace(" ", "_").lower() + self.sleep_duration = config.get('sleep_duration', 30) + + self.sensors = [] + self.init() + return + + def init(self): + for sensor in self.config['sensors']: + + if sensor.get('type', None) is not None: + # Get the sensor from the sensors folder + # {sensor name}_sensor.{SensorName}Sensor + sensor_type = 'sensors.pi.i2c.' + sensor_type += sensor.get('type').lower() + sensor_type += '_sensor.' + sensor_type += sensor.get('type').capitalize() + 'Sensor' + + imported_sensor = self.dynamic_import(sensor_type) + + # Define default kwargs for all sensor types, + # conditionally include optional variables below if they exist + sensor_kwargs = { + 'name': sensor.get('name', None), + 'address': int(sensor.get('address', 00)), + 'key': sensor.get('key', None) + } + + # Optional sensor variables + # Model is specific to DHT modules to specify + # DHT11 DHT22 or DHT2302 + if sensor.get('model'): + sensor_kwargs['model'] = str(sensor.get('model')) + + new_sensor = imported_sensor(**sensor_kwargs) + new_sensor.init_sensor() + + # Set the sensor type and determine if the + # readings are critical to operations + new_sensor.type = sensor.get('type').lower() + + self.sensors.append(new_sensor) + # print('{type} Sensor (Pi) + # {address}...\t\t\033[1;32m Ready\033[0;0m'.format(**sensor)) + return + + def run(self): + Logger.log(LOG_LEVEL["info"], 'Pi I2C Sensor Worker [' + str( + len(self.sensors)) + ' Sensors]...\t\033[1;32m Online\033[0;0m') + return super().run() + + def work(self): + readings = {} + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + + message = {'event': 'PiSensorUpdate'} + + for sensor in self.sensors: + result = sensor.read() + readings[sensor.key] = result + self.r.set(sensor.key, json.dumps(result)) + + message['data'] = readings + Logger.log(LOG_LEVEL["debug"], str(readings)) + self.r.publish(self.topic, json.dumps(message)) + time.sleep(self.sleep_duration) + + time.sleep(2) + # This is only ran after the main thread is shut down + Logger.log( + LOG_LEVEL["info"], + "Pi I2C Sensor Worker Shutting Down...\t\033[1;32m Complete\033[0;0m" + ) diff --git a/workers/linux/lcd_worker.py b/workers/linux/lcd_worker.py new file mode 100644 index 0000000..9a65bc1 --- /dev/null +++ b/workers/linux/lcd_worker.py @@ -0,0 +1,260 @@ +import re +import time +import json +import redis +import board +import busio +import datetime +import threading +import adafruit_character_lcd.character_lcd_rgb_i2c as character_rgb_lcd +import adafruit_character_lcd.character_lcd_i2c as character_lcd +from .worker import Worker + +from logger.Logger import Logger, LOG_LEVEL + + +class LcdWorker(Worker): + def __init__(self, config, main_thread_running, system_ready, + lcd_available): + super().__init__(config, main_thread_running, system_ready) + try: + self.address = int(self.config['address']) if self.config[ + 'address'] is not None else None + except KeyError: + self.address = None + + try: + self.model = str(self.config['model']) if self.config[ + 'model'] is not None else '' + except KeyError: + self.model = '' + + try: + self.columns = int(self.config['columns']) if self.config[ + 'columns'] is not None else 16 + except KeyError: + self.columns = 16 + + try: + self.rows = int(self.config['rows']) if self.config[ + 'rows'] is not None else 2 + except KeyError: + self.rows = 2 + + try: + self.default_duration = int(self.config['default_duration']) if \ + self.config['default_duration'] is not None else 5 + except KeyError: + self.default_duration = 5 + + self.current_message = "" + self.cached_message = { + 'message': '', + 'duration': self.default_duration + } + self.need_new_message = True + self.message_queue = [] + self.message_limit = int( + self.config['message_limit']) if self.config.get('message_limit', + None) is not None else 20 + + # Events + self.lcd_available = lcd_available + + # Dynamic Properties based on config + try: + self.topic = self.config['topic'].replace(" ", "/").lower() if \ + self.config['topic'] is not None else 'mudpi/lcd' + except KeyError: + self.topic = 'mudpi/lcd' + + # Pubsub Listeners + self.pubsub = self.r.pubsub() + self.pubsub.subscribe(**{self.topic: self.handle_message}) + + self.init() + return + + def init(self): + Logger.log( + LOG_LEVEL["info"], + 'LCD Display Worker...\t\t\t\033[1;32m Initializing\033[0;0m'.format( + **self.config) + ) + # prepare sensor on specified pin + self.i2c = busio.I2C(board.SCL, board.SDA) + + if (self.model): + + if (self.model.lower() == 'rgb'): + self.lcd = character_lcd.Character_LCD_RGB_I2C( + self.i2c, + self.columns, + self.rows, + self.address + ) + + elif (self.model.lower() == 'pcf'): + self.lcd = character_lcd.Character_LCD_I2C( + self.i2c, + self.columns, + self.rows, + address=self.address, + usingPCF=True + ) + else: + self.lcd = character_lcd.Character_LCD_I2C( + self.i2c, + self.columns, + self.rows, + self.address + ) + else: + self.lcd = character_lcd.Character_LCD_I2C( + self.i2c, self.columns, + self.rows, self.address + ) + + self.lcd.backlight = True + self.lcd.clear() + self.lcd.message = "MudPi\nGarden Online" + time.sleep(2) + self.lcd.clear() + return + + def run(self): + Logger.log( + LOG_LEVEL["info"], + 'LCD Display Worker ...\t\t\t\033[1;32m Online\033[0;0m'.format( + **self.config) + ) + return super().run() + + def handle_message(self, message): + data = message['data'] + if data is not None: + decoded_message = self.decode_message_data(data) + + try: + if decoded_message['event'] == 'Message': + if decoded_message.get('data', None): + self.add_message_to_queue( + decoded_message['data'].get('message', ''), + int( + decoded_message['data'].get( + 'duration', + self.default_duration + ) + ) + ) + Logger.log( + LOG_LEVEL["debug"], + 'LCD Message Queued: \033[1;36m{0}\033[0;0m'.format( + decoded_message['data'].get('message', + '')) + ) + + elif decoded_message['event'] == 'Clear': + self.lcd.clear() + Logger.log(LOG_LEVEL["debug"], 'Cleared the LCD Screen') + elif decoded_message['event'] == 'ClearQueue': + self.message_queue = [] + Logger.log(LOG_LEVEL["debug"], + 'Cleared the LCD Message Queue') + except Exception: + Logger.log(LOG_LEVEL["error"], + 'Error Decoding Message for LCD') + + def add_message_to_queue(self, message, duration=3): + # Add message to queue if LCD available + if self.lcd_available.is_set(): + + # Replace any codes such as [temperature] with a value + # found in redis + short_codes = re.findall(r'\[(.*?) *\]', message) + + for code in short_codes: + data = self.r.get(code) + + if data is None: + data = '' + + else: + try: + data = data.decode('utf-8') + except Exception: + data = '' + message = message.replace('[' + code + ']', str(data)) + + new_message = { + "message": message.replace("\\n", "\n"), + "duration": duration + } + + if len(self.message_queue) == self.message_limit: + self.message_queue.pop(0) + + self.message_queue.append(new_message) + + msg = {'event': 'MessageQueued', 'data': new_message} + self.r.publish(self.topic, json.dumps(msg)) + return + + def next_message_from_queue(self): + if len(self.message_queue) > 0: + self.need_new_message = False + self.reset_elapsed_time() + return self.message_queue.pop(0) + else: + time.sleep(3) # pause to reduce system load on message loop + return None + + def work(self): + self.reset_elapsed_time() + self.lcd.clear() + self.need_new_message = True + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + try: + self.pubsub.get_message() + if self.lcd_available.is_set(): + if self.cached_message and not self.need_new_message: + if self.current_message != self.cached_message['message']: + self.lcd.clear() + time.sleep(0.01) + self.lcd.message = self.cached_message[ + 'message'] + self.current_message = self.cached_message[ + 'message'] # store message to only display once and prevent flickers + if self.elapsed_time() > self.cached_message['duration'] + 1: + self.need_new_message = True + + else: + if self.need_new_message: + # Get first time message after clear + self.cached_message = self.next_message_from_queue() + else: + time.sleep(1) + + except Exception as e: + Logger.log( + LOG_LEVEL["error"], + "LCD Worker \t\033[1;31m Unexpected Error\033[0;0m".format( + **self.config) + ) + Logger.log(LOG_LEVEL["error"], "Exception: {0}".format(e)) + else: + # System not ready + time.sleep(1) + self.reset_elapsed_time() + + time.sleep(0.1) + + # This is only ran after the main thread is shut down + # Close the pubsub connection + self.pubsub.close() + Logger.log( + LOG_LEVEL["info"], + "LCD Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m".format( + **self.config) + ) diff --git a/workers/linux/relay_worker.py b/workers/linux/relay_worker.py new file mode 100644 index 0000000..b0c7444 --- /dev/null +++ b/workers/linux/relay_worker.py @@ -0,0 +1,197 @@ +import json +import sys +import time + +import board +import digitalio + +from .worker import Worker + + + +from logger.Logger import Logger, LOG_LEVEL + + +class RelayWorker(Worker): + def __init__(self, config, main_thread_running, + system_ready, relay_available, relay_active): + super().__init__(config, main_thread_running, system_ready) + + if self.config.get('key', None) is None: + raise Exception('No "key" Found in Relay Config') + else: + self.key = self.config.get('key', '').replace(" ", "_").lower() + + if self.config.get('name', None) is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = self.config['name'] + + self.pin_obj = getattr(board, self.config['pin']) + + # Events + self.relay_available = relay_available + self.relay_active = relay_active + + # Dynamic Properties based on config + self.active = False + self.topic = self.config.get('topic', '').replace(" ", + "/").lower() if self.config.get( + 'topic', None) is not None else 'mudpi/relays/' + self.key + self.pin_state_off = True if self.config[ + 'normally_open'] is not None and \ + self.config['normally_open'] else False + self.pin_state_on = False if self.config[ + 'normally_open'] is not None and \ + self.config['normally_open'] else True + + # Pubsub Listeners + self.pubsub = self.r.pubsub() + self.pubsub.subscribe(**{self.topic: self.handle_message}) + + self.init() + return + + def init(self): + Logger.log( + LOG_LEVEL["info"], + 'Relay Worker {0}...\t\t\t\033[1;32m Initializing\033[0;0m'.format( + self.key) + ) + self.gpio_pin = digitalio.DigitalInOut(self.pin_obj) + self.gpio_pin.switch_to_output() + # Close the relay by default, we use the pin state + # we determined based on the config at init + self.gpio_pin.value = self.pin_state_off + time.sleep(0.1) + + # Feature to restore relay state in case of crash + # or unexpected shutdown. This will check for last state + # stored in redis and set relay accordingly + if (self.config.get('restore_last_known_state', + None) is not None and self.config.get( + 'restore_last_known_state', False) is True): + if self.r.get(self.key + '_state'): + self.gpio_pin.value = self.pin_state_on + Logger.log( + LOG_LEVEL["info"], + 'Restoring Relay \033[1;36m{0} On\033[0;0m'.format( + self.key) + ) + + # Logger.log( + # LOG_LEVEL["info"], + # 'Relay Worker {0}...\t\t\t\033[1;32m Ready\033[0;0m'.format(self.key) + # ) + return + + def run(self): + Logger.log( + LOG_LEVEL["info"], + 'Relay Worker {0}...\t\t\t\033[1;32m Online\033[0;0m'.format( + self.key) + ) + return super().run() + + def handle_message(self, message): + data = message['data'] + if data is not None: + decoded_message = self.decode_message_data(data) + + try: + if decoded_message['event'] == 'Switch': + if decoded_message.get('data', None): + self.relay_active.set() + + elif decoded_message.get('data', None) == 0: + self.relay_active.clear() + Logger.log( + LOG_LEVEL["info"], + 'Switch Relay \033[1;36m{0}\033[0;0m state to \033[1;36m{1}\033[0;0m'.format( + self.key, decoded_message['data']) + ) + + elif decoded_message['event'] == 'Toggle': + state = 'Off' if self.active else 'On' + + if self.relay_active.is_set(): + self.relay_active.clear() + + else: + self.relay_active.set() + Logger.log( + LOG_LEVEL["info"], + 'Toggle Relay \033[1;36m{0} {1} \033[0;0m'.format( + self.key, state) + ) + except Exception: + Logger.log( + LOG_LEVEL["error"], + 'Error Decoding Message for Relay {0}'.format( + self.key) + ) + + def turn_on(self): + # Turn on relay if its available + if self.relay_available.is_set(): + if not self.active: + self.gpio_pin.value = self.pin_state_on + message = {'event': 'StateChanged', 'data': 1} + self.r.set(self.key + '_state', 1) + self.r.publish(self.topic, json.dumps(message)) + self.active = True + # This is handled by the redis listener now + # self.relay_active.set() + self.reset_elapsed_time() + + def turn_off(self): + # Turn off volkeye to flip off relay + if self.relay_available.is_set(): + if self.active: + self.gpio_pin.value = self.pin_state_off + message = {'event': 'StateChanged', 'data': 0} + self.r.delete(self.key + '_state') + self.r.publish(self.topic, json.dumps(message)) + # This is handled by the redis listener now + # self.relay_active.clear() + self.active = False + self.reset_elapsed_time() + + def work(self): + self.reset_elapsed_time() + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + + try: + self.pubsub.get_message() + if self.relay_available.is_set(): + if self.relay_active.is_set(): + self.turn_on() + else: + self.turn_off() + else: + self.turn_off() + time.sleep(1) + except Exception: + Logger.log( + LOG_LEVEL["error"], + "Relay Worker \033[1;36m{0}\033[0;0m \t\033[1;31m Unexpected Error\033[0;0m".format( + self.key) + ) + + else: + # System not ready relay should be off + self.turn_off() + time.sleep(1) + self.reset_elapsed_time() + + time.sleep(0.1) + + # This is only ran after the main thread is shut down + # Close the pubsub connection + self.pubsub.close() + Logger.log( + LOG_LEVEL["info"], + "Relay Worker {0} Shutting Down...\t\033[1;32m Complete\033[0;0m".format( + self.key) + ) diff --git a/workers/linux/sensor_worker.py b/workers/linux/sensor_worker.py new file mode 100644 index 0000000..65d278e --- /dev/null +++ b/workers/linux/sensor_worker.py @@ -0,0 +1,98 @@ +import json +import sys +import time + +from .worker import Worker + + +from logger.Logger import Logger, LOG_LEVEL + + +class PiSensorWorker(Worker): + def __init__(self, config, main_thread_running, system_ready): + super().__init__(config, main_thread_running, system_ready) + self.topic = config.get('topic', 'sensors').replace(" ", "_").lower() + self.sleep_duration = config.get('sleep_duration', 30) + + self.sensors = [] + self.init() + return + + def init(self): + for sensor in self.config['sensors']: + if sensor.get('type', None) is not None: + # Get the sensor from the sensors folder + # {sensor name}_sensor.{SensorName}Sensor + sensor_type = 'sensors.linux.' + sensor_type += sensor.get('type').lower() + sensor_type += '_sensor.' + sensor_type += sensor.get('type').capitalize() + 'Sensor' + + imported_sensor = self.dynamic_import(sensor_type) + + # Define default kwargs for all sensor types, + # conditionally include optional variables below if they exist + sensor_kwargs = { + 'name': sensor.get('name', None), + 'pin': sensor.get('pin', None), + 'key': sensor.get('key', None) + } + + # optional sensor variables + # Model is specific to DHT modules to specify + # DHT11 DHT22 or DHT2302 + if sensor.get('model'): + sensor_kwargs['model'] = str(sensor.get('model')) + + new_sensor = imported_sensor(**sensor_kwargs) + new_sensor.init_sensor() + + # Set the sensor type and determine if the readings + # are critical to operations + new_sensor.type = sensor.get('type').lower() + if sensor.get('critical', None) is not None: + new_sensor.critical = True + else: + new_sensor.critical = False + + self.sensors.append(new_sensor) + # print('{type} Sensor (Pi) + # {pin}...\t\t\033[1;32m Ready\033[0;0m'.format(**sensor)) + return + + def run(self): + Logger.log( + LOG_LEVEL["info"], 'Pi Sensor Worker [' + str( + len(self.sensors) + ) + ' Sensors]...\t\t\033[1;32m Online\033[0;0m' + ) + return super().run() + + def work(self): + while self.main_thread_running.is_set(): + + if self.system_ready.is_set(): + message = {'event': 'PiSensorUpdate'} + readings = {} + + for sensor in self.sensors: + result = sensor.read() + + if result is not None: + readings[sensor.key] = result + self.r.set(sensor.key, json.dumps(result)) + # print(sensor.name, result) + + if bool(readings): + print(readings) + message['data'] = readings + self.r.publish(self.topic, json.dumps(message)) + time.sleep(self.sleep_duration) + + time.sleep(2) + + # This is only ran after the main thread is shut down + Logger.log( + LOG_LEVEL["info"], + "Pi Sensor Worker Shutting Down...\t\033[1;32m Complete\033[0;0m" + ) diff --git a/workers/linux/worker.py b/workers/linux/worker.py new file mode 100644 index 0000000..d680922 --- /dev/null +++ b/workers/linux/worker.py @@ -0,0 +1,91 @@ +import json +import sys +import threading +import time + +import redis + + + +from logger.Logger import Logger, LOG_LEVEL + + +class Worker: + """ + Base Worker Class responsible for handling its set of operations + and running on a thread + + """ + def __init__(self, config, main_thread_running, system_ready): + self.config = config + try: + self.r = config["redis"] + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) + self.topic = config.get('topic', 'mudpi').replace(" ", "_").lower() + self.sleep_duration = config.get('sleep_duration', 15) + + # Threading Events to Keep Everything in Sync + self.main_thread_running = main_thread_running + self.system_ready = system_ready + self.worker_available = threading.Event() + + self.components = [] + return + + def init(self): + # print('Worker...\t\t\t\033[1;32m Initializing\033[0;0m'.format(**control)) + pass + + def run(self): + thread = threading.Thread(target=self.work, args=()) + thread.start() + return thread + + def work(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + time.sleep(self.sleep_duration) + # This is only ran after the main thread is shut down + Logger.log( + LOG_LEVEL["info"], + "Worker Shutting Down...\t\033[1;32m Complete\033[0;0m" + ) + + def elapsed_time(self): + self.time_elapsed = time.perf_counter() - self.time_start + return self.time_elapsed + + def reset_elapsed_time(self): + self.time_start = time.perf_counter() + pass + + def dynamic_import(self, name): + # Split path of the class folder structure: + # {sensor name}_sensor . {SensorName}Sensor + components = name.split('.') + # Dynamically import root of component path + module = __import__(components[0]) + # Get component attributes + for component in components[1:]: + module = getattr(module, component) + return module + + def decode_message_data(self, message): + + if isinstance(message, dict): + # print('Dict Found') + return message + + elif isinstance(message.decode('utf-8'), str): + try: + temp = json.loads(message.decode('utf-8')) + # print('Json Found') + return temp + + except Exception: + # print('Json Error. Str Found') + return {'event': 'Unknown', 'data': message} + else: + # print('Failed to detect type') + return {'event': 'Unknown', 'data': message} diff --git a/workers/pi/camera_worker.py b/workers/pi/camera_worker.py deleted file mode 100644 index 0b6716e..0000000 --- a/workers/pi/camera_worker.py +++ /dev/null @@ -1,147 +0,0 @@ -import time -import datetime -import json -import redis -import threading -import sys -import os -import RPi.GPIO as GPIO -from picamera import PiCamera -from .worker import Worker -sys.path.append('..') - -from logger.Logger import Logger, LOG_LEVEL - - -class CameraWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, camera_available): - super().__init__(config, main_thread_running, system_ready) - self.pending_reset = False - - # Events - self.camera_available = camera_available - - # Dynamic Properties based on config - self.path = self.config['path'].replace(" ", "-") if self.config['path'] is not None else '/etc/mudpi/img/' - self.topic = self.config['topic'].replace(" ", "/").lower() if self.config['topic'] is not None else 'mudpi/camera/' - if self.config['resolution'] is not None: - self.resolutionX = int(self.config['resolution'].get('x', 1920)) - self.resolutionY = int(self.config['resolution'].get('y', 1080)) - if self.config['delay'] is not None: - self.hours = int(self.config['delay'].get('hours', 0)) - self.minutes = int(self.config['delay'].get('minutes', 0)) - self.seconds = int(self.config['delay'].get('seconds', 0)) - - self.init() - return - - def init(self): - try: - self.camera = PiCamera(resolution=(self.resolutionX, self.resolutionY)) - # Below we calibrate the camera for consistent imaging - self.camera.framerate = 30 - # Wait for the automatic gain control to settle - time.sleep(2) - # Now fix the values - self.camera.shutter_speed = self.camera.exposure_speed - self.camera.exposure_mode = 'off' - g = self.camera.awb_gains - self.camera.awb_mode = 'off' - self.camera.awb_gains = g - except: - self.camera = PiCamera() - - #Pubsub Listeners - self.pubsub = self.r.pubsub() - self.pubsub.subscribe(**{self.topic: self.handleEvent}) - - Logger.log(LOG_LEVEL["info"], 'Camera Worker...\t\t\t\033[1;32m Ready\033[0;0m') - return - - def run(self): - t = threading.Thread(target=self.work, args=()) - t.start() - self.listener = threading.Thread(target=self.listen, args=()) - self.listener.start() - Logger.log(LOG_LEVEL["info"], 'Camera Worker...\t\t\t\033[1;32m Online\033[0;0m') - return t - - def wait(self): - # Calculate the delay - try: - self.next_time = (datetime.datetime.now() + datetime.timedelta(hours=self.hours, minutes=self.minutes, seconds=self.seconds)).replace(microsecond=0) - except: - # Default every hour - self.next_time = (datetime.datetime.now() + datetime.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0) - delay = (self.next_time - datetime.datetime.now()).seconds - time.sleep(delay) - - def handleEvent(self, message): - data = message['data'] - decoded_message = None - if data is not None: - try: - if isinstance(data, dict): - decoded_message = data - elif isinstance(data.decode('utf-8'), str): - temp = json.loads(data.decode('utf-8')) - decoded_message = temp - if decoded_message['event'] == 'Timelapse': - Logger.log(LOG_LEVEL["info"], "Camera Signaled for Reset") - self.camera_available.clear() - self.pending_reset = True - except: - Logger.log(LOG_LEVEL["error"], 'Error Handling Event for Camera') - - def listen(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - if self.camera_available.is_set(): - self.pubsub.get_message() - time.sleep(1) - else: - delay = (self.next_time - datetime.datetime.now()).seconds + 15 - time.sleep(delay) #wait 15 seconds after next scheduled picture - self.camera_available.set() - else: - time.sleep(2) - return - - def work(self): - self.resetElapsedTime() - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - if self.camera_available.is_set(): - # try: - for i, filename in enumerate(self.camera.capture_continuous(self.path + 'mudpi-{counter:05d}.jpg')): - if not self.camera_available.is_set(): - if self.pending_reset: - try: - os.remove(filename) #cleanup previous file - self.pending_reset = False - except: - Logger.log(LOG_LEVEL["error"], "Error During Camera Reset Cleanup") - break; - message = {'event':'StateChanged', 'data':filename} - self.r.set('last_camera_image', filename) - self.r.publish(self.topic, json.dumps(message)) - Logger.log(LOG_LEVEL["debug"], 'Image Captured \033[1;36m%s\033[0;0m' % filename) - self.wait() - # except: - # print("Camera Worker \t\033[1;31m Unexpected Error\033[0;0m") - # time.sleep(30) - else: - time.sleep(1) - self.resetElapsedTime() - else: - #System not ready camera should be off - time.sleep(1) - self.resetElapsedTime() - - time.sleep(0.1) - - #This is only ran after the main thread is shut down - self.camera.close() - self.listener.join() - self.pubsub.close() - Logger.log(LOG_LEVEL["info"], "Camera Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m") \ No newline at end of file diff --git a/workers/pi/control_worker.py b/workers/pi/control_worker.py deleted file mode 100644 index 9ff0050..0000000 --- a/workers/pi/control_worker.py +++ /dev/null @@ -1,67 +0,0 @@ -import time -import datetime -import json -import redis -import threading -from .worker import Worker -import sys -sys.path.append('..') -from controls.pi.button_control import (ButtonControl) -from controls.pi.switch_control import (SwitchControl) - -from logger.Logger import Logger, LOG_LEVEL - -class PiControlWorker(Worker): - def __init__(self, config, main_thread_running, system_ready): - super().__init__(config, main_thread_running, system_ready) - self.topic = config.get('topic', 'controls').replace(" ", "_").lower() - self.sleep_duration = config.get('sleep_duration', 0.5) - - self.controls = [] - self.init() - return - - def init(self): - for control in self.config['controls']: - if control.get('type', None) is not None: - #Get the control from the controls folder {control name}_control.{ControlName}Control - control_type = 'controls.pi.' + control.get('type').lower() + '_control.' + control.get('type').capitalize() + 'Control' - - imported_control = self.dynamic_import(control_type) - #new_control = imported_control(control.get('pin'), name=control.get('name', control.get('type')), connection=self.connection, key=control.get('key', None)) - - # Define default kwargs for all control types, conditionally include optional variables below if they exist - control_kwargs = { - 'name' : control.get('name', None), - 'pin' : int(control.get('pin')), - 'key' : control.get('key', None), - 'topic': control.get('topic', None), - 'resistor': control.get('resistor', None), - 'edge_detection': control.get('edge_detection', None), - 'debounce': control.get('debounce', None) - } - - # optional control variables - # add conditional control vars here... - - new_control = imported_control(**control_kwargs) - - new_control.init_control() - self.controls.append(new_control) - Logger.log(LOG_LEVEL["info"], '{type} Control {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format(**control)) - return - - def run(self): - Logger.log(LOG_LEVEL["info"], 'Pi Control Worker [' + str(len(self.config['controls'])) + ' Controls]...\t\033[1;32m Online\033[0;0m') - return super().run() - - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - readings = {} - for control in self.controls: - result = control.read() - readings[control.key] = result - time.sleep(self.sleep_duration) - #This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "Pi Control Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") \ No newline at end of file diff --git a/workers/pi/i2c_worker.py b/workers/pi/i2c_worker.py deleted file mode 100644 index 6aad813..0000000 --- a/workers/pi/i2c_worker.py +++ /dev/null @@ -1,77 +0,0 @@ -import time -import datetime -import json -import redis -import threading -import sys -sys.path.append('..') -from .worker import Worker -from sensors.pi.i2c.bme680_sensor import (Bme680Sensor) - -from logger.Logger import Logger, LOG_LEVEL - - -class PiI2CWorker(Worker): - def __init__(self, config, main_thread_running, system_ready): - super().__init__(config, main_thread_running, system_ready) - self.topic = config.get('topic', 'i2c').replace(" ", "_").lower() - self.sleep_duration = config.get('sleep_duration', 30) - - self.sensors = [] - self.init() - return - - def init(self): - for sensor in self.config['sensors']: - if sensor.get('type', None) is not None: - #Get the sensor from the sensors folder {sensor name}_sensor.{SensorName}Sensor - sensor_type = 'sensors.pi.i2c.' + sensor.get('type').lower() + '_sensor.' + sensor.get('type').capitalize() + 'Sensor' - - imported_sensor = self.dynamic_import(sensor_type) - - # Define default kwargs for all sensor types, conditionally include optional variables below if they exist - sensor_kwargs = { - 'name' : sensor.get('name', None), - 'address' : int(sensor.get('address', 00)), - 'key' : sensor.get('key', None) - } - - # Optional sensor variables - # Model is specific to DHT modules to specify DHT11 DHT22 or DHT2302 - if sensor.get('model'): - sensor_kwargs['model'] = str(sensor.get('model')) - - new_sensor = imported_sensor(**sensor_kwargs) - new_sensor.init_sensor() - - #Set the sensor type and determine if the readings are critical to operations - new_sensor.type = sensor.get('type').lower() - - self.sensors.append(new_sensor) - # print('{type} Sensor (Pi) {address}...\t\t\033[1;32m Ready\033[0;0m'.format(**sensor)) - return - - def run(self): - Logger.log(LOG_LEVEL["info"], 'Pi I2C Sensor Worker [' + str(len(self.sensors)) + ' Sensors]...\t\033[1;32m Online\033[0;0m') - return super().run() - - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - - message = {'event':'PiSensorUpdate'} - readings = {} - - for sensor in self.sensors: - result = sensor.read() - readings[sensor.key] = result - self.r.set(sensor.key, json.dumps(result)) - - message['data'] = readings - Logger.log(LOG_LEVEL["debug"], readings); - self.r.publish(self.topic, json.dumps(message)) - time.sleep(self.sleep_duration) - - time.sleep(2) - #This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "Pi I2C Sensor Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") \ No newline at end of file diff --git a/workers/pi/lcd_worker.py b/workers/pi/lcd_worker.py deleted file mode 100644 index 1200af0..0000000 --- a/workers/pi/lcd_worker.py +++ /dev/null @@ -1,182 +0,0 @@ -import re -import time -import json -import redis -import board -import busio -import datetime -import threading -import adafruit_character_lcd.character_lcd_rgb_i2c as character_rgb_lcd -import adafruit_character_lcd.character_lcd_i2c as character_lcd -from .worker import Worker - -from logger.Logger import Logger, LOG_LEVEL - -class LcdWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, lcd_available): - super().__init__(config, main_thread_running, system_ready) - try: - self.address = int(self.config['address']) if self.config['address'] is not None else None - except KeyError: - self.address = None - try: - self.model = str(self.config['model']) if self.config['model'] is not None else '' - except KeyError: - self.model = '' - try: - self.columns = int(self.config['columns']) if self.config['columns'] is not None else 16 - except KeyError: - self.columns = 16 - try: - self.rows = int(self.config['rows']) if self.config['rows'] is not None else 2 - except KeyError: - self.rows = 2 - try: - self.default_duration = int(self.config['default_duration']) if self.config['default_duration'] is not None else 5 - except KeyError: - self.default_duration = 5 - - self.current_message = "" - self.cached_message = {'message':'', 'duration': self.default_duration} - self.need_new_message = True - self.message_queue = [] - self.message_limit = int(self.config['message_limit']) if self.config.get('message_limit', None) is not None else 20 - - # Events - self.lcd_available = lcd_available - - # Dynamic Properties based on config - try: - self.topic = self.config['topic'].replace(" ", "/").lower() if self.config['topic'] is not None else 'mudpi/lcd' - except KeyError: - self.topic = 'mudpi/lcd' - - # Pubsub Listeners - self.pubsub = self.r.pubsub() - self.pubsub.subscribe(**{self.topic: self.handleMessage}) - - self.init() - return - - def init(self): - Logger.log(LOG_LEVEL["info"], 'LCD Display Worker...\t\t\t\033[1;32m Initializing\033[0;0m'.format(**self.config)) - # prepare sensor on specified pin - self.i2c = busio.I2C(board.SCL, board.SDA) - if(self.model): - if (self.model.lower() == 'rgb'): - self.lcd = character_lcd.Character_LCD_RGB_I2C(self.i2c, self.columns, self.rows, self.address) - elif (self.model.lower() == 'pcf'): - self.lcd = character_lcd.Character_LCD_I2C(self.i2c, self.columns, self.rows, address=self.address, usingPCF=True) - else: - self.lcd = character_lcd.Character_LCD_I2C(self.i2c, self.columns, self.rows, self.address) - else: - self.lcd = character_lcd.Character_LCD_I2C(self.i2c, self.columns, self.rows, self.address) - - self.lcd.backlight = True - self.lcd.clear() - self.lcd.message = "MudPi\nGarden Online" - time.sleep(2) - self.lcd.clear() - return - - def run(self): - Logger.log(LOG_LEVEL["info"], 'LCD Display Worker ...\t\t\t\033[1;32m Online\033[0;0m'.format(**self.config)) - return super().run() - - def handleMessage(self, message): - data = message['data'] - if data is not None: - decoded_message = self.decodeMessageData(data) - try: - if decoded_message['event'] == 'Message': - if decoded_message.get('data', None): - self.addMessageToQueue(decoded_message['data'].get('message', ''), int(decoded_message['data'].get('duration', self.default_duration))) - Logger.log(LOG_LEVEL["debug"], 'LCD Message Queued: \033[1;36m{0}\033[0;0m'.format(decoded_message['data'].get('message', ''))) - - elif decoded_message['event'] == 'Clear': - self.lcd.clear() - Logger.log(LOG_LEVEL["debug"], 'Cleared the LCD Screen') - elif decoded_message['event'] == 'ClearQueue': - self.message_queue = [] - Logger.log(LOG_LEVEL["debug"], 'Cleared the LCD Message Queue') - except: - Logger.log(LOG_LEVEL["error"], 'Error Decoding Message for LCD') - - def addMessageToQueue(self, message, duration = 3): - #Add message to queue if LCD available - if self.lcd_available.is_set(): - - # Replace any codes such as [temperature] with a value found in redis - short_codes = re.findall(r'\[(.*?) *\]', message) - - for code in short_codes: - data = self.r.get(code) - if data is None: - data = '' - else: - try: - data = data.decode('utf-8') - except: - data = '' - message = message.replace('['+code+']', str(data)) - - new_message = { - "message": message.replace("\\n", "\n"), - "duration": duration - } - - if len(self.message_queue) == self.message_limit: - self.message_queue.pop(0) - - self.message_queue.append(new_message) - - msg = { 'event':'MessageQueued', 'data': new_message } - self.r.publish(self.topic, json.dumps(msg)) - return - - def nextMessageFromQueue(self): - if len(self.message_queue) > 0: - self.need_new_message = False - self.resetElapsedTime() - return self.message_queue.pop(0) - else: - time.sleep(3) # pause to reduce system load on message loop - return None - - def work(self): - self.resetElapsedTime() - self.lcd.clear() - self.need_new_message = True - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - try: - self.pubsub.get_message() - if self.lcd_available.is_set(): - if self.cached_message and not self.need_new_message: - if self.current_message != self.cached_message['message']: - self.lcd.clear() - time.sleep(0.01) - self.lcd.message = self.cached_message['message'] - self.current_message = self.cached_message['message'] # store message to only display once and prevent flickers - if self.elapsedTime() > self.cached_message['duration'] + 1: - self.need_new_message = True - else: - if self.need_new_message: - # Get first time message after clear - self.cached_message = self.nextMessageFromQueue() - else: - time.sleep(1) - except Exception as e: - Logger.log(LOG_LEVEL["error"], "LCD Worker \t\033[1;31m Unexpected Error\033[0;0m".format(**self.config)) - Logger.log(LOG_LEVEL["error"], "Exception: {0}".format(e)) - else: - # System not ready - time.sleep(1) - self.resetElapsedTime() - - time.sleep(0.1) - - #This is only ran after the main thread is shut down - #Close the pubsub connection - self.pubsub.close() - Logger.log(LOG_LEVEL["info"], "LCD Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m".format(**self.config)) \ No newline at end of file diff --git a/workers/pi/relay_worker.py b/workers/pi/relay_worker.py deleted file mode 100644 index 6737679..0000000 --- a/workers/pi/relay_worker.py +++ /dev/null @@ -1,141 +0,0 @@ -import time -import datetime -import json -import redis -import threading -import sys -import RPi.GPIO as GPIO -from .worker import Worker -sys.path.append('..') - -from logger.Logger import Logger, LOG_LEVEL - -class RelayWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, relay_available, relay_active): - super().__init__(config, main_thread_running, system_ready) - - if self.config.get('key', None) is None: - raise Exception('No "key" Found in Relay Config') - else: - self.key = self.config.get('key', '').replace(" ", "_").lower() - - if self.config.get('name', None) is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = self.config['name'] - - self.config['pin'] = int(self.config['pin']) # parse possbile strings to avoid errors - - # Events - self.relay_available = relay_available - self.relay_active = relay_active - - # Dynamic Properties based on config - self.active = False - self.topic = self.config.get('topic', '').replace(" ", "/").lower() if self.config.get('topic', None) is not None else 'mudpi/relays/'+self.key - self.pin_state_off = GPIO.HIGH if self.config['normally_open'] is not None and self.config['normally_open'] else GPIO.LOW - self.pin_state_on = GPIO.LOW if self.config['normally_open'] is not None and self.config['normally_open'] else GPIO.HIGH - - # Pubsub Listeners - self.pubsub = self.r.pubsub() - self.pubsub.subscribe(**{self.topic: self.handleMessage}) - - self.init() - return - - def init(self): - Logger.log(LOG_LEVEL["info"], 'Relay Worker {0}...\t\t\t\033[1;32m Initializing\033[0;0m'.format(self.key)) - GPIO.setup(self.config['pin'], GPIO.OUT) - #Close the relay by default, we use the pin state we determined based on the config at init - GPIO.output(self.config['pin'], self.pin_state_off) - time.sleep(0.1) - - #Feature to restore relay state in case of crash or unexpected shutdown. This will check for last state stored in redis and set relay accordingly - if(self.config.get('restore_last_known_state', None) is not None and self.config.get('restore_last_known_state', False) is True): - if(self.r.get(self.key+'_state')): - GPIO.output(self.config['pin'], self.pin_state_on) - Logger.log(LOG_LEVEL["info"], 'Restoring Relay \033[1;36m{0} On\033[0;0m'.format(self.key)) - - - # Logger.log(LOG_LEVEL["info"], 'Relay Worker {0}...\t\t\t\033[1;32m Ready\033[0;0m'.format(self.key)) - return - - def run(self): - Logger.log(LOG_LEVEL["info"], 'Relay Worker {0}...\t\t\t\033[1;32m Online\033[0;0m'.format(self.key)) - return super().run() - - def handleMessage(self, message): - data = message['data'] - if data is not None: - decoded_message = self.decodeMessageData(data) - try: - if decoded_message['event'] == 'Switch': - if decoded_message.get('data', None): - self.relay_active.set() - elif decoded_message.get('data', None) == 0: - self.relay_active.clear() - Logger.log(LOG_LEVEL["info"], 'Switch Relay \033[1;36m{0}\033[0;0m state to \033[1;36m{1}\033[0;0m'.format(self.key, decoded_message['data'])) - elif decoded_message['event'] == 'Toggle': - state = 'Off' if self.active else 'On' - if self.relay_active.is_set(): - self.relay_active.clear() - else: - self.relay_active.set() - Logger.log(LOG_LEVEL["info"], 'Toggle Relay \033[1;36m{0} {1} \033[0;0m'.format(self.key, state)) - except: - Logger.log(LOG_LEVEL["error"], 'Error Decoding Message for Relay {0}'.format(self.key)) - - def turnOn(self): - #Turn on relay if its available - if self.relay_available.is_set(): - if not self.active: - GPIO.output(self.config['pin'], self.pin_state_on) - message = {'event':'StateChanged', 'data':1} - self.r.set(self.key+'_state', 1) - self.r.publish(self.topic, json.dumps(message)) - self.active = True - #self.relay_active.set() This is handled by the redis listener now - self.resetElapsedTime() - - def turnOff(self): - #Turn off volkeye to flip off relay - if self.relay_available.is_set(): - if self.active: - GPIO.output(self.config['pin'], self.pin_state_off) - message = {'event':'StateChanged', 'data':0} - self.r.delete(self.key+'_state') - self.r.publish(self.topic, json.dumps(message)) - #self.relay_active.clear() This is handled by the redis listener now - self.active = False - self.resetElapsedTime() - - def work(self): - self.resetElapsedTime() - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - - try: - self.pubsub.get_message() - if self.relay_available.is_set(): - if self.relay_active.is_set(): - self.turnOn() - else: - self.turnOff() - else: - self.turnOff() - time.sleep(1) - except: - Logger.log(LOG_LEVEL["error"], "Relay Worker \033[1;36m{0}\033[0;0m \t\033[1;31m Unexpected Error\033[0;0m".format(self.key)) - - else: - #System not ready relay should be off - self.turnOff() - time.sleep(1) - self.resetElapsedTime() - - time.sleep(0.1) - - # This is only ran after the main thread is shut down - # Close the pubsub connection - self.pubsub.close() - Logger.log(LOG_LEVEL["info"], "Relay Worker {0} Shutting Down...\t\033[1;32m Complete\033[0;0m".format(self.key)) diff --git a/workers/pi/sensor_worker.py b/workers/pi/sensor_worker.py deleted file mode 100644 index f2e2722..0000000 --- a/workers/pi/sensor_worker.py +++ /dev/null @@ -1,82 +0,0 @@ -import time -import datetime -import json -import redis -import threading -from .worker import Worker -import sys -sys.path.append('..') -from sensors.pi.float_sensor import (FloatSensor) -from sensors.pi.humidity_sensor import (HumiditySensor) -from logger.Logger import Logger, LOG_LEVEL - -class PiSensorWorker(Worker): - def __init__(self, config, main_thread_running, system_ready): - super().__init__(config, main_thread_running, system_ready) - self.topic = config.get('topic', 'sensors').replace(" ", "_").lower() - self.sleep_duration = config.get('sleep_duration', 30) - - self.sensors = [] - self.init() - return - - def init(self): - for sensor in self.config['sensors']: - if sensor.get('type', None) is not None: - #Get the sensor from the sensors folder {sensor name}_sensor.{SensorName}Sensor - sensor_type = 'sensors.pi.' + sensor.get('type').lower() + '_sensor.' + sensor.get('type').capitalize() + 'Sensor' - - imported_sensor = self.dynamic_import(sensor_type) - - # Define default kwargs for all sensor types, conditionally include optional variables below if they exist - sensor_kwargs = { - 'name' : sensor.get('name', None), - 'pin' : int(sensor.get('pin', 0)), - 'key' : sensor.get('key', None) - } - - # optional sensor variables - # Model is specific to DHT modules to specify DHT11 DHT22 or DHT2302 - if sensor.get('model'): - sensor_kwargs['model'] = str(sensor.get('model')) - - new_sensor = imported_sensor(**sensor_kwargs) - new_sensor.init_sensor() - - #Set the sensor type and determine if the readings are critical to operations - new_sensor.type = sensor.get('type').lower() - if sensor.get('critical', None) is not None: - new_sensor.critical = True - else: - new_sensor.critical = False - - self.sensors.append(new_sensor) - # print('{type} Sensor (Pi) {pin}...\t\t\033[1;32m Ready\033[0;0m'.format(**sensor)) - return - - def run(self): - Logger.log(LOG_LEVEL["info"], 'Pi Sensor Worker [' + str(len(self.sensors)) + ' Sensors]...\t\t\033[1;32m Online\033[0;0m') - return super().run() - - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - message = {'event':'PiSensorUpdate'} - readings = {} - for sensor in self.sensors: - result = sensor.read() - if result is not None: - readings[sensor.key] = result - self.r.set(sensor.key, json.dumps(result)) - #print(sensor.name, result) - - if bool(readings): - print(readings) - message['data'] = readings - self.r.publish(self.topic, json.dumps(message)) - time.sleep(self.sleep_duration) - - time.sleep(2) - - # This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "Pi Sensor Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") \ No newline at end of file diff --git a/workers/pi/worker.py b/workers/pi/worker.py deleted file mode 100644 index a1c4e97..0000000 --- a/workers/pi/worker.py +++ /dev/null @@ -1,80 +0,0 @@ -import time -import datetime -import json -import redis -import threading -import sys -sys.path.append('..') - -import variables -from logger.Logger import Logger, LOG_LEVEL - -# Base Worker Class -# A worker is responsible for handling its set of operations and running on a thread -class Worker(): - def __init__(self, config, main_thread_running, system_ready): - self.config = config - try: - self.r = config["redis"] - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - self.topic = config.get('topic', 'mudpi').replace(" ", "_").lower() - self.sleep_duration = config.get('sleep_duration', 15) - - # Threading Events to Keep Everything in Sync - self.main_thread_running = main_thread_running - self.system_ready = system_ready - self.worker_available = threading.Event() - - self.components = [] - return - - def init(self): - # print('Worker...\t\t\t\033[1;32m Initializing\033[0;0m'.format(**control)) - return - - def run(self): - t = threading.Thread(target=self.work, args=()) - t.start() - return t - - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - time.sleep(self.sleep_duration) - #This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") - - def elapsedTime(self): - self.time_elapsed = time.perf_counter() - self.time_start - return self.time_elapsed - - def resetElapsedTime(self): - self.time_start = time.perf_counter() - pass - - def dynamic_import(self, name): - # Split path of the class folder structure: {sensor name}_sensor . {SensorName}Sensor - components = name.split('.') - # Dynamically import root of component path - module = __import__(components[0]) - # Get component attributes - for component in components[1:]: - module = getattr(module, component) - return module - - def decodeMessageData(self, message): - if isinstance(message, dict): - #print('Dict Found') - return message - elif isinstance(message.decode('utf-8'), str): - try: - temp = json.loads(message.decode('utf-8')) - #print('Json Found') - return temp - except: - #print('Json Error. Str Found') - return {'event':'Unknown', 'data':message} - else: - #print('Failed to detect type') - return {'event':'Unknown', 'data':message} \ No newline at end of file diff --git a/workers/sequence_worker.py b/workers/sequence_worker.py index 01ad576..c938b02 100644 --- a/workers/sequence_worker.py +++ b/workers/sequence_worker.py @@ -5,294 +5,358 @@ import threading from .worker import Worker import sys -sys.path.append('..') -import variables + + +import constants import importlib from logger.Logger import Logger, LOG_LEVEL + class SequenceWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, sequence_available, sequence_active, actions): - super().__init__(config, main_thread_running, system_ready) + def __init__(self, config, main_thread_running, system_ready, + sequence_available, sequence_active, actions): + super().__init__(config, main_thread_running, system_ready) + + if self.config.get('key', None) is None: + raise Exception('No "key" Found in Sequence Config') + else: + self.key = self.config['key'].replace(" ", "_").lower() - if self.config.get('key', None) is None: - raise Exception('No "key" Found in Sequence Config') - else: - self.key = self.config['key'].replace(" ", "_").lower() + if self.config.get('name', None) is None: + self.name = self.key.replace("_", " ").title() + else: + self.name = self.config.get('name', '') - if self.config.get('name', None) is None: - self.name = self.key.replace("_", " ").title() - else: - self.name = self.config.get('name', '') + self.actions = actions + self.sequence = self.config['sequence'] if self.config[ + 'sequence'] is not None else [] + self.key = self.config.get( + 'key', '').replace(" ", "_").lower() if self.config.get( + 'key', None) is not None else self.name.replace(" ", "_").lower() + self.topic = self.config.get( + 'topic', '').replace(" ", "/").lower() if self.config.get( + 'topic', None) is not None else 'mudpi/sequences/' + self.key + self.current_step = 0 + self.total_steps = len(self.sequence) + self.delay_complete = False + self.step_complete = False + self.step_triggered = False - self.actions = actions - self.sequence = self.config['sequence'] if self.config['sequence'] is not None else [] - self.key = self.config.get('key', '').replace(" ", "_").lower() if self.config.get('key', None) is not None else self.name.replace(" ", "_").lower() - self.topic = self.config.get('topic', '').replace(" ", "/").lower() if self.config.get('topic', None) is not None else 'mudpi/sequences/'+self.key - self.current_step = 0 - self.total_steps = len(self.sequence) - self.delay_complete = False - self.step_complete = False - self.step_triggered = False + # Events + self.sequence_available = sequence_available + self.sequence_active = sequence_active - # Events - self.sequence_available = sequence_available - self.sequence_active = sequence_active + # PubSub + try: + self.r = config["redis"] + except KeyError: + self.r = redis.Redis(host='127.0.0.1', port=6379) - # PubSub - try: - self.r = config["redis"] - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) + # Pubsub Listeners + self.pubsub = self.r.pubsub() - # Pubsub Listeners - self.pubsub = self.r.pubsub() + self.reset_elapsed_time() + self.init() + return - self.resetElapsedTime() - self.init() - return + def init(self): + self.pubsub.subscribe(**{self.topic: self.handle_message}) + # Logger.log(LOG_LEVEL["info"], '{type} - {name}...\t\t\033[1;32m Listening\033[0;0m'.format(**trigger)) + return - def init(self): - self.pubsub.subscribe(**{self.topic: self.handleMessage}) - # Logger.log(LOG_LEVEL["info"], '{type} - {name}...\t\t\033[1;32m Listening\033[0;0m'.format(**trigger)) - return + def reset_step(self): + self.delay_complete = False + self.step_triggered = False + self.step_complete = False - def reset_step(self): - self.delay_complete = False - self.step_triggered = False - self.step_complete = False + def update(self, value=None): + if not self.sequence_active.is_set(): + self.start() + else: + if self.sequence[self.current_step].get('duration', + None) is None and self.delay_complete: + self.step_complete = True + self.next_step() - def update(self, value=None): - if not self.sequence_active.is_set(): - self.start() - else: - if self.sequence[self.current_step].get('duration', None) is None and self.delay_complete: - self.step_complete = True - self.next_step() + def start(self): + if not self.sequence_active.is_set(): + self.current_step = 0 + self.sequence_active.set() + self.reset_step() + self.r.publish(self.topic, json.dumps({ + "event": "SequenceStarted", + "data": { + "name": self.name, + "key": self.key + } + })) + Logger.log( + LOG_LEVEL["info"], + 'Sequence {0} Started\033[0;0m'.format(self.name) + ) - def start(self): - if not self.sequence_active.is_set(): - self.current_step = 0 - self.sequence_active.set() - self.reset_step() - self.r.publish(self.topic, json.dumps({ - "event": "SequenceStarted", - "data": { - "name": self.name, - "key": self.key - } - })) - Logger.log(LOG_LEVEL["info"], 'Sequence {0} Started\033[0;0m'.format(self.name)) + def stop(self): + if self.sequence_active.is_set(): + self.current_step = 0 + self.sequence_active.clear() + self.reset_step() + self.r.publish(self.topic, json.dumps({ + "event": "SequenceStopped", + "data": { + "name": self.name, + "key": self.key + } + })) + Logger.log( + LOG_LEVEL["info"], + 'Sequence {0} Stopped\033[0;0m'.format(self.name) + ) - def stop(self): - if self.sequence_active.is_set(): - self.current_step = 0 - self.sequence_active.clear() - self.reset_step() - self.r.publish(self.topic, json.dumps({ - "event": "SequenceStopped", - "data": { - "name": self.name, - "key": self.key - } - })) - Logger.log(LOG_LEVEL["info"], 'Sequence {0} Stopped\033[0;0m'.format(self.name)) + def next_step(self): + if self.step_complete: + # Step must be flagged complete to advance + if self.sequence_active.is_set(): + # If skipping steps trigger unperformed actions + if not self.step_triggered: + if self.evaluate_thresholds(): + self.trigger() + # Sequence is already active, advance to next step + if self.current_step < self.total_steps - 1: + self.reset_step() + self.current_step += 1 + self.r.publish( + self.topic, + json.dumps( + { + "event": "SequenceStepStarted", + "data": { + "name": self.name, + "key": self.key, + "step": self.current_step + } + } + ) + ) + else: + # Last step of seqence completed + self.sequence_active.clear() + self.r.publish(self.topic, json.dumps({ + "event": "SequenceEnded", + "data": { + "name": self.name, + "key": self.key + } + })) + self.reset_elapsed_time() - def next_step(self): - if self.step_complete: - # Step must be flagged complete to advance - if self.sequence_active.is_set(): - # If skipping steps trigger unperformed actions - if not self.step_triggered: - if self.evaluateThresholds(): - self.trigger() - # Sequence is already active, advance to next step - if self.current_step < self.total_steps - 1: - self.reset_step() - self.current_step+=1 - self.r.publish(self.topic, json.dumps({ - "event": "SequenceStepStarted", - "data": { - "name": self.name, - "key": self.key, - "step": self.current_step - } - })) - else: - # Last step of seqence completed - self.sequence_active.clear() - self.r.publish(self.topic, json.dumps({ - "event": "SequenceEnded", - "data": { - "name": self.name, - "key": self.key - } - })) - self.resetElapsedTime() + def previous_step(self): + if self.current_step > 0: + self.reset_step() + self.current_step -= 1 + self.r.publish(self.topic, json.dumps({ + "event": "SequenceStepStarted", + "data": { + "name": self.name, + "key": self.key, + "step": self.current_step + } + })) + self.reset_elapsed_time() - def previous_step(self): - if self.current_step > 0: - self.reset_step() - self.current_step-=1 - self.r.publish(self.topic, json.dumps({ - "event": "SequenceStepStarted", - "data": { - "name": self.name, - "key": self.key, - "step": self.current_step - } - })) - self.resetElapsedTime() + def handle_message(self, message): + data = message['data'] + if data is not None: + decoded_message = self.last_event = self.decode_message_data(data) + try: + if decoded_message['event'] == 'SequenceNextStep': + if self.sequence[self.current_step].get('duration', + None) is None and self.delay_complete: + self.step_complete = True + Logger.log( + LOG_LEVEL["info"], + 'Sequence {0} Next Step Triggered\033[0;0m'.format( + self.name) + ) + elif decoded_message['event'] == 'SequencePreviousStep': + self.previous_step() + Logger.log( + LOG_LEVEL["info"], + 'Sequence {0} Previous Step Triggered\033[0;0m'.format( + self.name) + ) + elif decoded_message['event'] == 'SequenceStart': + self.start() + Logger.log( + LOG_LEVEL["info"], + 'Sequence {0} Start Triggered\033[0;0m'.format( + self.name) + ) + elif decoded_message['event'] == 'SequenceSkipStep': + self.step_complete = True + Logger.log( + LOG_LEVEL["info"], + 'Sequence {0} Skip Step Triggered\033[0;0m'.format( + self.name) + ) + elif decoded_message['event'] == 'SequenceStop': + self.stop() + Logger.log( + LOG_LEVEL["info"], + 'Sequence {0} Stop Triggered\033[0;0m'.format( + self.name) + ) + except: + Logger.log( + LOG_LEVEL["info"], + 'Error Decoding Message for Sequence {0}'.format( + self.config['key']) + ) - def handleMessage(self, message): - data = message['data'] - if data is not None: - decoded_message = self.last_event = self.decodeMessageData(data) - try: - if decoded_message['event'] == 'SequenceNextStep': - if self.sequence[self.current_step].get('duration', None) is None and self.delay_complete: - self.step_complete = True - Logger.log(LOG_LEVEL["info"], 'Sequence {0} Next Step Triggered\033[0;0m'.format(self.name)) - elif decoded_message['event'] == 'SequencePreviousStep': - self.previous_step() - Logger.log(LOG_LEVEL["info"], 'Sequence {0} Previous Step Triggered\033[0;0m'.format(self.name)) - elif decoded_message['event'] == 'SequenceStart': - self.start() - Logger.log(LOG_LEVEL["info"], 'Sequence {0} Start Triggered\033[0;0m'.format(self.name)) - elif decoded_message['event'] == 'SequenceSkipStep': - self.step_complete = True - Logger.log(LOG_LEVEL["info"], 'Sequence {0} Skip Step Triggered\033[0;0m'.format(self.name)) - elif decoded_message['event'] == 'SequenceStop': - self.stop() - Logger.log(LOG_LEVEL["info"], 'Sequence {0} Stop Triggered\033[0;0m'.format(self.name)) - except: - Logger.log(LOG_LEVEL["info"], 'Error Decoding Message for Sequence {0}'.format(self.config['key'])) + def trigger(self, value=None): + try: + for action in self.sequence[self.current_step].get('actions', []): + self.actions[action].trigger(value) + self.step_triggered = True + except Exception as e: + Logger.log(LOG_LEVEL["error"], + "Error triggering sequence action {0} ".format( + self.key), e) + pass + return - def trigger(self, value=None): - try: - for action in self.sequence[self.current_step].get('actions', []): - self.actions[action].trigger(value) - self.step_triggered = True - except Exception as e: - Logger.log(LOG_LEVEL["error"], "Error triggering sequence action {0} ".format(self.key), e) - pass - return + def evaluate_thresholds(self): + thresholds_passed = False + if self.sequence[self.current_step].get('thresholds', + None) is not None: + for threshold in self.sequence[self.current_step].get('thresholds', + []): + key = threshold.get("source", None) + data = self.r.get(key) + if data is not None: + if threshold.get("nested_source", None) is not None: + nested_source = threshold['nested_source'].lower() + data = json.loads(data.decode('utf-8')) + value = data.get(nested_source, + None) if nested_source is not None else data + comparison = threshold.get("comparison", "eq") + if comparison == "eq": + if value == threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + elif comparison == "ne": + if value != threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + elif comparison == "gt": + if value > threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + elif comparison == "gte": + if value >= threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + elif comparison == "lt": + if value < threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + elif comparison == "lte": + if value <= threshold["value"]: + thresholds_passed = True + else: + thresholds_passed = False + else: + # No thresholds for this step, proceed. + thresholds_passed = True + return thresholds_passed - def evaluateThresholds(self): - thresholds_passed = False - if self.sequence[self.current_step].get('thresholds', None) is not None: - for threshold in self.sequence[self.current_step].get('thresholds', []): - key = threshold.get("source", None) - data = self.r.get(key) - if data is not None: - if threshold.get("nested_source", None) is not None: - nested_source = threshold['nested_source'].lower() - data = json.loads(data.decode('utf-8')) - value = data.get(nested_source, None) if nested_source is not None else data - comparison = threshold.get("comparison", "eq") - if comparison == "eq": - if value == threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "ne": - if value != threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "gt": - if value > threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "gte": - if value >= threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "lt": - if value < threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - elif comparison == "lte": - if value <= threshold["value"]: - thresholds_passed = True - else: - thresholds_passed = False - else: - # No thresholds for this step, proceed. - thresholds_passed = True - return thresholds_passed + def wait(self, duration=0): + time_remaining = duration + while time_remaining > 0 and self.main_thread_running.is_set(): + self.pubsub.get_message() + time.sleep(1) + time_remaining -= 1 - def wait(self, duration=0): - time_remaining = duration - while time_remaining > 0 and self.main_thread_running.is_set(): - self.pubsub.get_message() - time.sleep(1) - time_remaining-=1 + def run(self): + Logger.log(LOG_LEVEL["info"], 'Sequence Worker [' + str( + self.name) + ']...\t\033[1;32m Online\033[0;0m') + return super().run() - def run(self): - Logger.log(LOG_LEVEL["info"], 'Sequence Worker [' + str(self.name) + ']...\t\033[1;32m Online\033[0;0m') - return super().run() + def work(self): + self.reset_elapsed_time() + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + try: + self.pubsub.get_message() + if self.sequence_available.is_set(): + if self.sequence_active.is_set(): + while not self.step_complete: + if not self.delay_complete: + if self.sequence[self.current_step].get( + 'delay', None) is not None: + self.wait(int(self.sequence[ + self.current_step].get( + 'delay', 0))) + self.delay_complete = True + else: + # No Delay for this step + self.delay_complete = True + if not self.step_triggered: + if self.delay_complete: + if self.evaluate_thresholds(): + self.trigger() + else: + if self.sequence[ + self.current_step].get( + 'thresholds', + None) is not None: + # Thresholds failed skip step waiting + self.step_complete = True + if self.sequence[self.current_step].get( + 'duration', + None) is not None and not self.step_complete: + self.wait(int( + self.sequence[self.current_step].get( + 'duration', 0))) + self.step_complete = True + time.sleep(1) + if self.step_complete: + self.r.publish(self.topic, json.dumps({ + "event": "SequenceStepEnded", + "data": { + "name": self.name, + "key": self.key, + "step": self.current_step + } + })) + self.next_step() + else: + # Sequence not active and waiting to start + time.sleep(1) + else: + # Sequence Disabled + time.sleep(1) + except Exception as e: + Logger.log(LOG_LEVEL["error"], + "Sequence Worker {0} \t\033[1;31m Unexpected Error\033[0;0m".format( + self.key)) + Logger.log(LOG_LEVEL["error"], e) + time.sleep(3) + else: + # System not ready + time.sleep(1) + self.reset_elapsed_time() - def work(self): - self.resetElapsedTime() - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - try: - self.pubsub.get_message() - if self.sequence_available.is_set(): - if self.sequence_active.is_set(): - while not self.step_complete: - if not self.delay_complete: - if self.sequence[self.current_step].get('delay', None) is not None: - self.wait(int(self.sequence[self.current_step].get('delay', 0))) - self.delay_complete = True - else: - # No Delay for this step - self.delay_complete = True - if not self.step_triggered: - if self.delay_complete: - if self.evaluateThresholds(): - self.trigger() - else: - if self.sequence[self.current_step].get('thresholds', None) is not None: - # Thresholds failed skip step waiting - self.step_complete = True - if self.sequence[self.current_step].get('duration', None) is not None and not self.step_complete: - self.wait(int(self.sequence[self.current_step].get('duration', 0))) - self.step_complete = True - time.sleep(1) - if self.step_complete: - self.r.publish(self.topic, json.dumps({ - "event": "SequenceStepEnded", - "data": { - "name": self.name, - "key": self.key, - "step": self.current_step - } - })) - self.next_step() - else: - # Sequence not active and waiting to start - time.sleep(1) - else: - # Sequence Disabled - time.sleep(1) - except Exception as e: - Logger.log(LOG_LEVEL["error"], "Sequence Worker {0} \t\033[1;31m Unexpected Error\033[0;0m".format(self.key)) - Logger.log(LOG_LEVEL["error"], e) - time.sleep(3) - else: - # System not ready - time.sleep(1) - self.resetElapsedTime() - - time.sleep(0.1) + time.sleep(0.1) - # This is only ran after the main thread is shut down - # Close the pubsub connection - self.pubsub.close() - Logger.log(LOG_LEVEL["info"], "Sequence Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") \ No newline at end of file + # This is only ran after the main thread is shut down + # Close the pubsub connection + self.pubsub.close() + Logger.log(LOG_LEVEL["info"], + "Sequence Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") diff --git a/workers/trigger_worker.py b/workers/trigger_worker.py index c36fc2e..57a1de3 100644 --- a/workers/trigger_worker.py +++ b/workers/trigger_worker.py @@ -5,136 +5,150 @@ import threading from .worker import Worker import sys -sys.path.append('..') + + from triggers.time_trigger import TimeTrigger from triggers.trigger_group import TriggerGroup from triggers.sensor_trigger import SensorTrigger from triggers.control_trigger import ControlTrigger -import variables +import constants import importlib from logger.Logger import Logger, LOG_LEVEL + class TriggerWorker(Worker): - def __init__(self, config, main_thread_running, system_ready, actions, sequences): - super().__init__(config, main_thread_running, system_ready) - self.actions = actions - self.sequences = sequences - self.triggers = [] - self.trigger_threads = [] - self.trigger_events = {} - self.init() - return - - def init(self): - trigger_index = 0 - for trigger in self.config: - if trigger.get("triggers", False): - # Load a trigger group - - trigger_actions = [] - if trigger.get('actions'): - for action in trigger.get("actions"): - trigger_actions.append(self.actions[action]) - - new_trigger_group = TriggerGroup(name=trigger.get("group"), key=trigger.get("key"), actions=trigger_actions, frequency=trigger.get("frequency", "once")) - - for trigger in trigger.get("triggers"): - new_trigger = self.init_trigger(trigger, trigger_index, group=new_trigger_group) - self.triggers.append(new_trigger) - new_trigger_group.add_trigger(new_trigger) - # Start the trigger thread - trigger_thread = new_trigger.run() - self.trigger_threads.append(trigger_thread) - trigger_index += 1 - else: - new_trigger = self.init_trigger(trigger, trigger_index) - self.triggers.append(new_trigger) - # Start the trigger thread - trigger_thread = new_trigger.run() - self.trigger_threads.append(trigger_thread) - trigger_index += 1 - # print('{type} - {name}...\t\t\033[1;32m Listening\033[0;0m'.format(**trigger)) - return - - def init_trigger(self, config, trigger_index, group=None): - if config.get('type', None) is not None: - # Get the trigger from the triggers folder triggers/{trigger type}_trigger.py - trigger_type = 'triggers.' + config.get('type').lower() + '_trigger.' + config.get('type').capitalize() + 'Trigger' - - imported_trigger = self.dynamic_import(trigger_type) - - trigger_state = { - "active": threading.Event() # Event to signal if trigger is active - } - - self.trigger_events[config.get("key", trigger_index)] = trigger_state - - # Define default kwargs for all trigger types, conditionally include optional variables below if they exist - trigger_kwargs = { - 'name' : config.get('name', None), - 'key' : config.get('key', None), - 'trigger_active' : trigger_state["active"], - 'main_thread_running' : self.main_thread_running, - 'system_ready' : self.system_ready - } - - # optional trigger variables - if config.get('actions'): - trigger_actions = [] - for action in config.get("actions"): - trigger_actions.append(self.actions[action]) - trigger_kwargs['actions'] = trigger_actions - - if config.get('sequences'): - trigger_sequences = [] - for sequence in config.get("sequences"): - trigger_sequences.append(self.sequences[sequence]) - trigger_kwargs['sequences'] = trigger_sequences - - if config.get('frequency'): - trigger_kwargs['frequency'] = config.get('frequency') - - if config.get('schedule'): - trigger_kwargs['schedule'] = config.get('schedule') - - if config.get('source'): - trigger_kwargs['source'] = config.get('source') - - if config.get('nested_source'): - trigger_kwargs['nested_source'] = config.get('nested_source') - - if config.get('topic'): - trigger_kwargs['topic'] = config.get('topic') - - if config.get('thresholds'): - trigger_kwargs['thresholds'] = config.get('thresholds') - - if group is not None: - trigger_kwargs['group'] = group - - new_trigger = imported_trigger(**trigger_kwargs) - new_trigger.init_trigger() - - new_trigger.type = config.get('type').lower() - - return new_trigger - - def run(self): - Logger.log(LOG_LEVEL["info"], 'Trigger Worker [' + str(len(self.config)) + ' Triggers]...\t\t\033[1;32m Online\033[0;0m') - return super().run() - - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - # Main Loop - time.sleep(1) - - time.sleep(2) - # This is only ran after the main thread is shut down - for trigger in self.triggers: - trigger.shutdown() - # Join all our sub threads for shutdown - for thread in self.trigger_threads: - thread.join() - Logger.log(LOG_LEVEL["info"], "Trigger Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m") \ No newline at end of file + def __init__(self, config, main_thread_running, system_ready, actions, + sequences): + super().__init__(config, main_thread_running, system_ready) + self.actions = actions + self.sequences = sequences + self.triggers = [] + self.trigger_threads = [] + self.trigger_events = {} + self.init() + return + + def init(self): + trigger_index = 0 + for trigger in self.config: + if trigger.get("triggers", False): + # Load a trigger group + + trigger_actions = [] + if trigger.get('actions'): + for action in trigger.get("actions"): + trigger_actions.append(self.actions[action]) + + new_trigger_group = TriggerGroup(name=trigger.get("group"), + key=trigger.get("key"), + actions=trigger_actions, + frequency=trigger.get( + "frequency", "once")) + + for trigger in trigger.get("triggers"): + new_trigger = self.init_trigger(trigger, trigger_index, + group=new_trigger_group) + self.triggers.append(new_trigger) + new_trigger_group.add_trigger(new_trigger) + # Start the trigger thread + trigger_thread = new_trigger.run() + self.trigger_threads.append(trigger_thread) + trigger_index += 1 + else: + new_trigger = self.init_trigger(trigger, trigger_index) + self.triggers.append(new_trigger) + # Start the trigger thread + trigger_thread = new_trigger.run() + self.trigger_threads.append(trigger_thread) + trigger_index += 1 + # print('{type} - {name}...\t\t\033[1;32m Listening\033[0;0m'.format(**trigger)) + return + + def init_trigger(self, config, trigger_index, group=None): + if config.get('type', None) is not None: + # Get the trigger from the triggers folder triggers/{trigger type}_trigger.py + trigger_type = 'triggers.' + config.get( + 'type').lower() + '_trigger.' + config.get( + 'type').capitalize() + 'Trigger' + + imported_trigger = self.dynamic_import(trigger_type) + + trigger_state = { + "active": threading.Event() + # Event to signal if trigger is active + } + + self.trigger_events[ + config.get("key", trigger_index)] = trigger_state + + # Define default kwargs for all trigger types, conditionally include optional variables below if they exist + trigger_kwargs = { + 'name': config.get('name', None), + 'key': config.get('key', None), + 'trigger_active': trigger_state["active"], + 'main_thread_running': self.main_thread_running, + 'system_ready': self.system_ready + } + + # optional trigger variables + if config.get('actions'): + trigger_actions = [] + for action in config.get("actions"): + trigger_actions.append(self.actions[action]) + trigger_kwargs['actions'] = trigger_actions + + if config.get('sequences'): + trigger_sequences = [] + for sequence in config.get("sequences"): + trigger_sequences.append(self.sequences[sequence]) + trigger_kwargs['sequences'] = trigger_sequences + + if config.get('frequency'): + trigger_kwargs['frequency'] = config.get('frequency') + + if config.get('schedule'): + trigger_kwargs['schedule'] = config.get('schedule') + + if config.get('source'): + trigger_kwargs['source'] = config.get('source') + + if config.get('nested_source'): + trigger_kwargs['nested_source'] = config.get('nested_source') + + if config.get('topic'): + trigger_kwargs['topic'] = config.get('topic') + + if config.get('thresholds'): + trigger_kwargs['thresholds'] = config.get('thresholds') + + if group is not None: + trigger_kwargs['group'] = group + + new_trigger = imported_trigger(**trigger_kwargs) + new_trigger.init_trigger() + + new_trigger.type = config.get('type').lower() + + return new_trigger + + def run(self): + Logger.log(LOG_LEVEL["info"], 'Trigger Worker [' + str( + len(self.config)) + ' Triggers]...\t\t\033[1;32m Online\033[0;0m') + return super().run() + + def work(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + # Main Loop + time.sleep(1) + + time.sleep(2) + # This is only ran after the main thread is shut down + for trigger in self.triggers: + trigger.shutdown() + # Join all our sub threads for shutdown + for thread in self.trigger_threads: + thread.join() + Logger.log(LOG_LEVEL["info"], + "Trigger Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m") diff --git a/workers/worker.py b/workers/worker.py index a5da114..b1f91c7 100644 --- a/workers/worker.py +++ b/workers/worker.py @@ -4,70 +4,78 @@ import redis import threading import sys -sys.path.append('..') -import variables + + +import constants from logger.Logger import Logger, LOG_LEVEL -# Base Worker Class -# A worker is responsible for handling its set of operations and running on a thread -class Worker(): - def __init__(self, config, main_thread_running, system_ready): - self.config = config - # Threading Events to Keep Everything in Sync - self.main_thread_running = main_thread_running - self.system_ready = system_ready - self.worker_available = threading.Event() - self.components = [] - return +class Worker: + """Base Worker Class + + A worker is responsible for handling its set of operations and + running on a thread + """ + + def __init__(self, config, main_thread_running, system_ready): + self.config = config + # Threading Events to Keep Everything in Sync + self.main_thread_running = main_thread_running + self.system_ready = system_ready + self.worker_available = threading.Event() + + self.components = [] + return - def init(self): - # print('Worker...\t\t\t\033[1;32m Initializing\033[0;0m'.format(**control)) - return + def init(self): + # print('Worker...\t\t\t\033[1;32m Initializing\033[0;0m'.format(**control)) + return - def run(self): - t = threading.Thread(target=self.work, args=()) - t.start() - return t + def run(self): + t = threading.Thread(target=self.work, args=()) + t.start() + return t - def work(self): - while self.main_thread_running.is_set(): - if self.system_ready.is_set(): - time.sleep(self.sleep_duration) - #This is only ran after the main thread is shut down - Logger.log(LOG_LEVEL["info"], "Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") + def work(self): + while self.main_thread_running.is_set(): + if self.system_ready.is_set(): + time.sleep(self.sleep_duration) + # This is only ran after the main thread is shut down + Logger.log(LOG_LEVEL["info"], + "Worker Shutting Down...\t\033[1;32m Complete\033[0;0m") - def elapsedTime(self): - self.time_elapsed = time.perf_counter() - self.time_start - return self.time_elapsed + def elapsed_time(self): + self.time_elapsed = time.perf_counter() - self.time_start + return self.time_elapsed - def resetElapsedTime(self): - self.time_start = time.perf_counter() - pass + def reset_elapsed_time(self): + self.time_start = time.perf_counter() + pass - def dynamic_import(self, name): - # Split path of the class folder structure: {sensor name}_sensor . {SensorName}Sensor - components = name.split('.') - # Dynamically import root of component path - module = __import__(components[0]) - # Get component attributes - for component in components[1:]: - module = getattr(module, component) - return module + def dynamic_import(self, name): + # Split path of the class folder structure: + # {sensor name}_sensor . {SensorName}Sensor + components = name.split('.') + # Dynamically import root of component path + module = __import__(components[0]) + # Get component attributes + for component in components[1:]: + module = getattr(module, component) + return module - def decodeMessageData(self, message): - if isinstance(message, dict): - #print('Dict Found') - return message - elif isinstance(message.decode('utf-8'), str): - try: - temp = json.loads(message.decode('utf-8')) - #print('Json Found') - return temp - except: - #print('Json Error. Str Found') - return {'event':'Unknown', 'data':message} - else: - #print('Failed to detect type') - return {'event':'Unknown', 'data':message} \ No newline at end of file + def decode_message_data(self, message): + if isinstance(message, dict): + # print('Dict Found') + return message + elif isinstance(message.decode('utf-8'), str): + try: + temp = json.loads(message.decode('utf-8')) + # print('Json Found') + return temp + except: + # print('Json Error. Str Found') + return {'event': 'Unknown', 'data': message} + else: + # print('Failed to detect type') + return {'event': 'Unknown', 'data': message}