From 48340592fdbda94f419246f5d552e8c247e17258 Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Tue, 16 Mar 2021 09:58:28 +0100 Subject: [PATCH 1/9] Fix Logger class attributes Make a default attribute as a standalone logger. --- mudpi/logger/Logger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mudpi/logger/Logger.py b/mudpi/logger/Logger.py index d50ab9b..e361206 100644 --- a/mudpi/logger/Logger.py +++ b/mudpi/logger/Logger.py @@ -15,7 +15,7 @@ class Logger: - logger = None + logger = logging.getLogger("mudpi_stream") def __init__(self, config: dict): if "logging" in config.keys(): @@ -106,7 +106,7 @@ def log(log_level, msg: str): # for ease of access from outside log_level = LOG_LEVEL[log_level] else: log_level = LOG_LEVEL['unknown'] - Logger.logger.log_this(log_level, msg) + Logger.logger.log(log_level, msg) @staticmethod def log_formatted(log_level, message, status='', status_level=None, padding=FONT_PADDING, spacer="."): From f63f732e38f47c9c03d621b92b65558768829b43 Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Tue, 16 Mar 2021 09:59:06 +0100 Subject: [PATCH 2/9] Fix minor issue on logging Error when logging to the console. --- mudpi/importer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mudpi/importer.py b/mudpi/importer.py index 205adb3..bccc661 100644 --- a/mudpi/importer.py +++ b/mudpi/importer.py @@ -78,9 +78,9 @@ def get_extension_importer(mudpi, extension, install_requirements=False): 'Ready', 'success' ) else: - Logger.log_formatted( - "error", f'Import Preperations for {extension.title()}', - 'Failed', 'error' + Logger.log_formatted(LOG_LEVEL["debug"], + f'Import Preperations for {extension.title()}', + 'error', 'error' ) Logger.log( LOG_LEVEL["debug"], From c5a4a24d30b1dde0623fae60168eca95f2cef227 Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Thu, 18 Mar 2021 08:59:51 +0100 Subject: [PATCH 3/9] Add some PEP fix to the code for more readability --- build/lib/mudpi/controls/arduino/control.py | 2 +- build/lib/mudpi/controls/linux/control.py | 2 +- build/lib/mudpi/events/__init__.py | 2 +- build/lib/mudpi/extensions/example/char_display.py | 2 +- build/lib/mudpi/extensions/i2c/char_display.py | 4 ++-- build/lib/mudpi/extensions/rtsp/camera.py | 2 +- build/lib/mudpi/extensions/trigger/__init__.py | 2 +- build/lib/mudpi/importer.py | 2 +- build/lib/mudpi/managers/state_manager.py | 4 ++-- build/lib/mudpi/sensors/arduino/rain_sensor.py | 14 +++++++------- build/lib/mudpi/sensors/arduino/soil_sensor.py | 6 +++--- .../mudpi/sensors/arduino/temperature_sensor.py | 2 +- build/lib/mudpi/sensors/linux/i2c/sensor.py | 2 +- build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py | 4 ++-- .../mudpi/workers/arduino/arduino_relay_worker.py | 2 +- build/lib/mudpi/workers/arduino/worker.py | 2 +- build/lib/mudpi/workers/linux/display_worker.py | 6 +++--- mudpi/debug/dump.py | 4 ++-- mudpi/extensions/dht/sensor.py | 1 + mudpi/extensions/gpio/sensor.py | 2 ++ 20 files changed, 35 insertions(+), 32 deletions(-) diff --git a/build/lib/mudpi/controls/arduino/control.py b/build/lib/mudpi/controls/arduino/control.py index a7eae44..762dec8 100644 --- a/build/lib/mudpi/controls/arduino/control.py +++ b/build/lib/mudpi/controls/arduino/control.py @@ -8,7 +8,7 @@ default_connection = SerialManager() # Base sensor class to extend all other arduino sensors from. -class Control(): +class Control: def __init__(self, pin, name=None, connection=default_connection, analog_pin_mode=False, key=None, redis_conn=None): self.pin = pin diff --git a/build/lib/mudpi/controls/linux/control.py b/build/lib/mudpi/controls/linux/control.py index 21a1377..e895d95 100644 --- a/build/lib/mudpi/controls/linux/control.py +++ b/build/lib/mudpi/controls/linux/control.py @@ -15,7 +15,7 @@ # Base sensor class to extend all other arduino sensors from. -class Control(): +class Control: def __init__(self, pin, name=None, key=None, resistor=None, edge_detection=None, debounce=None, redis_conn=None): if key is None: diff --git a/build/lib/mudpi/events/__init__.py b/build/lib/mudpi/events/__init__.py index 08a1c27..7552e43 100644 --- a/build/lib/mudpi/events/__init__.py +++ b/build/lib/mudpi/events/__init__.py @@ -10,7 +10,7 @@ from mudpi.logger.Logger import Logger, LOG_LEVEL -class EventSystem(): +class EventSystem: """ Main event manager that loads adaptors and coordinates the bus operations. """ diff --git a/build/lib/mudpi/extensions/example/char_display.py b/build/lib/mudpi/extensions/example/char_display.py index d6d20b2..1f2660c 100644 --- a/build/lib/mudpi/extensions/example/char_display.py +++ b/build/lib/mudpi/extensions/example/char_display.py @@ -17,7 +17,7 @@ def load(self, config): # Check for test messages to fill the queue with if config.get('messages'): _count = 0 - while(_count < display.message_limit): + while _count < display.message_limit: for msg in config['messages']: display.add_message({'message': msg}) _count +=1 diff --git a/build/lib/mudpi/extensions/i2c/char_display.py b/build/lib/mudpi/extensions/i2c/char_display.py index 9cf8ef3..704ba4a 100644 --- a/build/lib/mudpi/extensions/i2c/char_display.py +++ b/build/lib/mudpi/extensions/i2c/char_display.py @@ -106,7 +106,7 @@ def init(self): # Prepare the display i2c connection self.i2c = busio.I2C(board.SCL, board.SDA) - if (self.model == 'rgb'): + if self.model == 'rgb': self.lcd = character_lcd.Character_LCD_RGB_I2C( self.i2c, self.columns, @@ -114,7 +114,7 @@ def init(self): self.address ) - elif (self.model == 'pcf'): + elif self.model == 'pcf': self.lcd = character_lcd.Character_LCD_I2C( self.i2c, self.columns, diff --git a/build/lib/mudpi/extensions/rtsp/camera.py b/build/lib/mudpi/extensions/rtsp/camera.py index cef1bc3..32367c4 100644 --- a/build/lib/mudpi/extensions/rtsp/camera.py +++ b/build/lib/mudpi/extensions/rtsp/camera.py @@ -144,7 +144,7 @@ def capture_recording(self, data={}): _writer = cv2.VideoWriter(_file_name, self.fourcc, self.framerate, self.size) _start = time.perf_counter() - while(self.cap.isOpened()): + while self.cap.isOpened(): ret, frame = self.cap.read() if ret and frame is not None: frame = cv2.resize(frame, self.size, interpolation = cv2.INTER_AREA) diff --git a/build/lib/mudpi/extensions/trigger/__init__.py b/build/lib/mudpi/extensions/trigger/__init__.py index 7c68bc8..7bcf0a1 100644 --- a/build/lib/mudpi/extensions/trigger/__init__.py +++ b/build/lib/mudpi/extensions/trigger/__init__.py @@ -224,4 +224,4 @@ def split_trigger_configs(config): else: _triggers.append(conf) - return (_triggers, _groups) + return _triggers, _groups diff --git a/build/lib/mudpi/importer.py b/build/lib/mudpi/importer.py index 381b622..cff7417 100644 --- a/build/lib/mudpi/importer.py +++ b/build/lib/mudpi/importer.py @@ -282,7 +282,7 @@ def prepare_interface_and_import(self, interface_name): else: extension = self.mudpi.extensions.get(interface_name) - return (interface, extension) + return interface, extension def import_extension(self, config): diff --git a/build/lib/mudpi/managers/state_manager.py b/build/lib/mudpi/managers/state_manager.py index 086c5b2..6c1d954 100644 --- a/build/lib/mudpi/managers/state_manager.py +++ b/build/lib/mudpi/managers/state_manager.py @@ -7,7 +7,7 @@ from mudpi.logger.Logger import Logger, LOG_LEVEL -class StateManager(): +class StateManager: """ A Central Manager to Control All States in MudPi. @@ -109,7 +109,7 @@ def restore_states(self): self.states[key] = _state -class State(): +class State: """ A Class for Stored State from Components """ diff --git a/build/lib/mudpi/sensors/arduino/rain_sensor.py b/build/lib/mudpi/sensors/arduino/rain_sensor.py index 984b88e..edb386a 100644 --- a/build/lib/mudpi/sensors/arduino/rain_sensor.py +++ b/build/lib/mudpi/sensors/arduino/rain_sensor.py @@ -47,17 +47,17 @@ def read_raw(self): return resistance def parseSensorReading(self, raw_data): - if (raw_data > MIST_BOUNDS): + if raw_data > MIST_BOUNDS: return 'No Rain' - elif (raw_data <= MIST_BOUNDS and raw_data > LIGHT_RAIN_BOUNDS): + elif MIST_BOUNDS >= raw_data > LIGHT_RAIN_BOUNDS: return 'Mist' - elif (raw_data <= LIGHT_RAIN_BOUNDS and raw_data > RAIN_BOUNDS): + elif LIGHT_RAIN_BOUNDS >= raw_data > RAIN_BOUNDS: return 'Light Rain' - elif (raw_data <= RAIN_BOUNDS and raw_data > HEAVY_RAIN_BOUNDS): + elif RAIN_BOUNDS >= raw_data > HEAVY_RAIN_BOUNDS: return 'Rain' - elif (raw_data <= HEAVY_RAIN_BOUNDS and raw_data > DOWNPOUR_BOUNDS): + elif HEAVY_RAIN_BOUNDS >= raw_data > DOWNPOUR_BOUNDS: return 'Heavy Rain' - elif (raw_data <= DOWNPOUR_BOUNDS): + elif raw_data <= DOWNPOUR_BOUNDS: return 'Downpour' else: return 'Bad Sensor Data' @@ -66,7 +66,7 @@ def parseSensorReading(self, raw_data): if __name__ == '__main__': try: loop_count = 10 - while (loop_count > 0): + while loop_count > 0: sensor = RainSensor(4) rainread = sensor.read() print('Rain: ', rainread) diff --git a/build/lib/mudpi/sensors/arduino/soil_sensor.py b/build/lib/mudpi/sensors/arduino/soil_sensor.py index 414ef88..25d980c 100644 --- a/build/lib/mudpi/sensors/arduino/soil_sensor.py +++ b/build/lib/mudpi/sensors/arduino/soil_sensor.py @@ -36,11 +36,11 @@ def read(self): resistance = self.api.analogRead(self.pin) moistpercent = ((resistance - WaterBounds) / ( AirBounds - WaterBounds)) * 100 - if (moistpercent > 80): + if moistpercent > 80: moisture = 'Very Dry - ' + str(int(moistpercent)) - elif (moistpercent <= 80 and moistpercent > 45): + elif 80 >= moistpercent > 45: moisture = 'Dry - ' + str(int(moistpercent)) - elif (moistpercent <= 45 and moistpercent > 25): + elif 45 >= moistpercent > 25: moisture = 'Wet - ' + str(int(moistpercent)) else: moisture = 'Very Wet - ' + str(int(moistpercent)) diff --git a/build/lib/mudpi/sensors/arduino/temperature_sensor.py b/build/lib/mudpi/sensors/arduino/temperature_sensor.py index 077c40e..ff8686e 100644 --- a/build/lib/mudpi/sensors/arduino/temperature_sensor.py +++ b/build/lib/mudpi/sensors/arduino/temperature_sensor.py @@ -65,7 +65,7 @@ def readAll(self): loop_count = 10 sensor = TemperatureSensor(2) sensor.init_sensor() - while (loop_count > 0): + while loop_count > 0: tempread = sensor.readAll() print('Temps: ', tempread) loop_count += 1 diff --git a/build/lib/mudpi/sensors/linux/i2c/sensor.py b/build/lib/mudpi/sensors/linux/i2c/sensor.py index 60d060e..c7bb4c2 100644 --- a/build/lib/mudpi/sensors/linux/i2c/sensor.py +++ b/build/lib/mudpi/sensors/linux/i2c/sensor.py @@ -7,7 +7,7 @@ # PIN MODE : OUT | IN -class Sensor(): +class Sensor: def __init__(self, address, name=None, key=None, redis_conn=None): self.address = address diff --git a/build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py b/build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py index 5eab007..4cff16a 100644 --- a/build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py +++ b/build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py @@ -17,10 +17,10 @@ def __init__(self, address=None, name=None, key=None, redis_conn=None): return def init_sensor(self): - '''This is the bus number : the 1 in "/dev/i2c-1" + """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.''' + We might encounter boards with several buses.""" self.bus = smbus.SMBus(1) return diff --git a/build/lib/mudpi/workers/arduino/arduino_relay_worker.py b/build/lib/mudpi/workers/arduino/arduino_relay_worker.py index 0dae496..24fc412 100644 --- a/build/lib/mudpi/workers/arduino/arduino_relay_worker.py +++ b/build/lib/mudpi/workers/arduino/arduino_relay_worker.py @@ -78,7 +78,7 @@ def init(self): 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')): + 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( diff --git a/build/lib/mudpi/workers/arduino/worker.py b/build/lib/mudpi/workers/arduino/worker.py index 9d0125e..e88f9ef 100644 --- a/build/lib/mudpi/workers/arduino/worker.py +++ b/build/lib/mudpi/workers/arduino/worker.py @@ -12,7 +12,7 @@ # Base Worker Class # A worker is responsible for handling its set of operations and running on a thread -class Worker(): +class Worker: def __init__(self, config, main_thread_running, system_ready): self.config = config try: diff --git a/build/lib/mudpi/workers/linux/display_worker.py b/build/lib/mudpi/workers/linux/display_worker.py index 745e27b..d4d2b72 100644 --- a/build/lib/mudpi/workers/linux/display_worker.py +++ b/build/lib/mudpi/workers/linux/display_worker.py @@ -85,9 +85,9 @@ def init(self): # prepare sensor on specified pin self.i2c = busio.I2C(board.SCL, board.SDA) - if (self.model): + if self.model: - if (self.model.lower() == 'rgb'): + if self.model.lower() == 'rgb': self.lcd = character_lcd.Character_LCD_RGB_I2C( self.i2c, self.columns, @@ -95,7 +95,7 @@ def init(self): self.address ) - elif (self.model.lower() == 'pcf'): + elif self.model.lower() == 'pcf': self.lcd = character_lcd.Character_LCD_I2C( self.i2c, self.columns, diff --git a/mudpi/debug/dump.py b/mudpi/debug/dump.py index ba41653..1f13937 100644 --- a/mudpi/debug/dump.py +++ b/mudpi/debug/dump.py @@ -107,9 +107,9 @@ def dumpall(): for x in a.register.names: r = a.register.get(x) if r.size == 2: - v = '0x%04X' % (r.value) + v = '0x%04X' % r.value else: - v = ' 0x%02X' % (r.value) + v = ' 0x%02X' % r.value print('%-20s = %s @0x%2X (size:%s)' % (r.name, v, r.address, r.size)) diff --git a/mudpi/extensions/dht/sensor.py b/mudpi/extensions/dht/sensor.py index 21307b5..8a8ad7f 100644 --- a/mudpi/extensions/dht/sensor.py +++ b/mudpi/extensions/dht/sensor.py @@ -9,6 +9,7 @@ from mudpi.extensions import BaseInterface from mudpi.extensions.sensor import Sensor from mudpi.exceptions import MudPiError, ConfigError +from mudpi.logger.Logger import Logger, LOG_LEVEL class Interface(BaseInterface): diff --git a/mudpi/extensions/gpio/sensor.py b/mudpi/extensions/gpio/sensor.py index 3b8d137..52a7d36 100644 --- a/mudpi/extensions/gpio/sensor.py +++ b/mudpi/extensions/gpio/sensor.py @@ -3,6 +3,8 @@ Connects to a linux board GPIO to take analog or digital readings. """ +import re + import board import digitalio from mudpi.extensions import BaseInterface From 60887ea52fc7688e3673b4eb2eb2f4f7646162d9 Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Thu, 18 Mar 2021 09:25:34 +0100 Subject: [PATCH 4/9] Add more PEP fixes --- mudpi/extensions/dht/sensor.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/mudpi/extensions/dht/sensor.py b/mudpi/extensions/dht/sensor.py index 8a8ad7f..3a09813 100644 --- a/mudpi/extensions/dht/sensor.py +++ b/mudpi/extensions/dht/sensor.py @@ -3,12 +3,12 @@ Connects to a DHT device to get humidity and temperature readings. """ -import re -import board import adafruit_dht +import board + +from mudpi.exceptions import ConfigError from mudpi.extensions import BaseInterface from mudpi.extensions.sensor import Sensor -from mudpi.exceptions import MudPiError, ConfigError from mudpi.logger.Logger import Logger, LOG_LEVEL @@ -30,14 +30,6 @@ def validate(self, config): if not conf.get('pin'): raise ConfigError('Missing `pin` in DHT config.') - if not re.match(r'D\d+$', conf['pin']) and not re.match(r'A\d+$', conf['pin']): - raise ConfigError( - "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" - ) - valid_models = ['11', '22', '2302'] if conf.get('model') not in valid_models: conf['model'] = '11' @@ -45,7 +37,7 @@ def validate(self, config): LOG_LEVEL["warning"], 'Sensor Model Error: Defaulting to DHT11' ) - + return config @@ -54,7 +46,6 @@ class DHTSensor(Sensor): Returns a random number """ - """ Properties """ @property def id(self): """ Return a unique id for the component """ @@ -64,7 +55,7 @@ def id(self): def name(self): """ Return the display name of the component """ return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - + @property def state(self): """ Return the state of the component (from memory, no IO!) """ @@ -75,8 +66,6 @@ def classifier(self): """ Classification further describing it, effects the data formatting """ return 'climate' - - """ Methods """ def init(self): """ Connect to the device """ self.pin_obj = getattr(board, self.config['pin']) @@ -112,7 +101,7 @@ def update(self): try: # Calling temperature or humidity triggers measure() - temperature_c = self._sensor.temperature + temperature_c = self._sensor.temperature humidity = self._sensor.humidity except RuntimeError as error: # Errors happen fairly often, DHT's are hard to read From b8a0dd4c4395d3ef57c8b9afb34ae9bcf612bb01 Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Thu, 18 Mar 2021 14:40:02 +0100 Subject: [PATCH 5/9] Add proper gitignore to avoid submitting code not needed --- .gitignore | 141 +++- build/lib/mudpi/__init__.py | 0 build/lib/mudpi/__main__.py | 204 ----- build/lib/mudpi/config.py | 201 ----- build/lib/mudpi/constants.py | 142 ---- build/lib/mudpi/controls/__init__.py | 0 build/lib/mudpi/controls/arduino/__init__.py | 0 .../mudpi/controls/arduino/button_control.py | 39 - build/lib/mudpi/controls/arduino/control.py | 61 -- .../controls/arduino/potentiometer_control.py | 38 - .../mudpi/controls/arduino/switch_control.py | 40 - build/lib/mudpi/controls/linux/__init__.py | 0 .../mudpi/controls/linux/button_control.py | 30 - build/lib/mudpi/controls/linux/control.py | 107 --- .../mudpi/controls/linux/switch_control.py | 42 - build/lib/mudpi/core.py | 236 ------ build/lib/mudpi/events/__init__.py | 84 -- build/lib/mudpi/events/adaptors/__init__.py | 54 -- build/lib/mudpi/events/adaptors/mqtt.py | 74 -- build/lib/mudpi/events/adaptors/redis.py | 55 -- build/lib/mudpi/exceptions.py | 41 - build/lib/mudpi/extensions/__init__.py | 335 -------- build/lib/mudpi/extensions/action/__init__.py | 118 --- build/lib/mudpi/extensions/bme680/__init__.py | 12 - build/lib/mudpi/extensions/bme680/sensor.py | 107 --- build/lib/mudpi/extensions/camera/__init__.py | 193 ----- .../mudpi/extensions/char_display/__init__.py | 222 ----- .../lib/mudpi/extensions/control/__init__.py | 91 --- build/lib/mudpi/extensions/control/trigger.py | 91 --- build/lib/mudpi/extensions/cron/__init__.py | 12 - build/lib/mudpi/extensions/cron/trigger.py | 75 -- build/lib/mudpi/extensions/dht/__init__.py | 15 - build/lib/mudpi/extensions/dht/sensor.py | 133 --- .../lib/mudpi/extensions/example/__init__.py | 12 - .../mudpi/extensions/example/char_display.py | 65 -- build/lib/mudpi/extensions/example/control.py | 90 --- build/lib/mudpi/extensions/example/sensor.py | 52 -- build/lib/mudpi/extensions/example/toggle.py | 64 -- build/lib/mudpi/extensions/gpio/__init__.py | 12 - build/lib/mudpi/extensions/gpio/control.py | 122 --- build/lib/mudpi/extensions/gpio/sensor.py | 86 -- build/lib/mudpi/extensions/gpio/toggle.py | 118 --- build/lib/mudpi/extensions/group/__init__.py | 11 - build/lib/mudpi/extensions/group/trigger.py | 83 -- build/lib/mudpi/extensions/i2c/__init__.py | 11 - .../lib/mudpi/extensions/i2c/char_display.py | 135 ---- build/lib/mudpi/extensions/mqtt/__init__.py | 92 --- build/lib/mudpi/extensions/mqtt/sensor.py | 149 ---- build/lib/mudpi/extensions/nanpy/__init__.py | 202 ----- .../mudpi/extensions/nanpy/char_display.py | 149 ---- build/lib/mudpi/extensions/nanpy/control.py | 151 ---- build/lib/mudpi/extensions/nanpy/sensor.py | 225 ------ build/lib/mudpi/extensions/nanpy/toggle.py | 139 ---- .../lib/mudpi/extensions/picamera/__init__.py | 12 - build/lib/mudpi/extensions/picamera/camera.py | 101 --- build/lib/mudpi/extensions/redis/__init__.py | 49 -- build/lib/mudpi/extensions/redis/sensor.py | 176 ---- build/lib/mudpi/extensions/rtsp/__init__.py | 13 - build/lib/mudpi/extensions/rtsp/camera.py | 177 ---- build/lib/mudpi/extensions/sensor/__init__.py | 49 -- build/lib/mudpi/extensions/sensor/trigger.py | 95 --- .../lib/mudpi/extensions/sequence/__init__.py | 481 ----------- build/lib/mudpi/extensions/socket/__init__.py | 151 ---- build/lib/mudpi/extensions/state/__init__.py | 12 - build/lib/mudpi/extensions/state/trigger.py | 88 -- build/lib/mudpi/extensions/sun/__init__.py | 31 - build/lib/mudpi/extensions/sun/sensor.py | 89 -- build/lib/mudpi/extensions/t9602/__init__.py | 13 - build/lib/mudpi/extensions/t9602/sensor.py | 101 --- build/lib/mudpi/extensions/toggle/__init__.py | 146 ---- build/lib/mudpi/extensions/toggle/trigger.py | 88 -- .../lib/mudpi/extensions/trigger/__init__.py | 227 ------ build/lib/mudpi/importer.py | 757 ------------------ build/lib/mudpi/logger/Logger.py | 167 ---- build/lib/mudpi/logger/__init__.py | 0 build/lib/mudpi/managers/__init__.py | 0 build/lib/mudpi/managers/core_manager.py | 261 ------ build/lib/mudpi/managers/extension_manager.py | 224 ------ build/lib/mudpi/managers/state_manager.py | 157 ---- build/lib/mudpi/mudpi_main.py | 293 ------- build/lib/mudpi/registry.py | 172 ---- build/lib/mudpi/sensors/__init__.py | 0 build/lib/mudpi/sensors/arduino/__init__.py | 0 .../lib/mudpi/sensors/arduino/float_sensor.py | 25 - .../mudpi/sensors/arduino/humidity_sensor.py | 52 -- .../lib/mudpi/sensors/arduino/light_sensor.py | 36 - .../lib/mudpi/sensors/arduino/rain_sensor.py | 78 -- build/lib/mudpi/sensors/arduino/sensor.py | 55 -- .../lib/mudpi/sensors/arduino/soil_sensor.py | 57 -- .../sensors/arduino/temperature_sensor.py | 76 -- build/lib/mudpi/sensors/base_sensor.py | 35 - build/lib/mudpi/sensors/linux/__init__.py | 0 build/lib/mudpi/sensors/linux/float_sensor.py | 32 - .../mudpi/sensors/linux/humidity_sensor.py | 104 --- build/lib/mudpi/sensors/linux/i2c/__init__.py | 0 .../mudpi/sensors/linux/i2c/bme680_sensor.py | 57 -- build/lib/mudpi/sensors/linux/i2c/sensor.py | 53 -- .../mudpi/sensors/linux/i2c/t9602_sensor.py | 53 -- build/lib/mudpi/sensors/linux/sensor.py | 57 -- build/lib/mudpi/sensors/mcp3xxx/__init__.py | 0 build/lib/mudpi/sensors/mcp3xxx/sensor.py | 45 -- .../lib/mudpi/sensors/mcp3xxx/soil_sensor.py | 49 -- build/lib/mudpi/utils.py | 102 --- build/lib/mudpi/workers/__init__.py | 108 --- build/lib/mudpi/workers/adc_worker.py | 138 ---- build/lib/mudpi/workers/arduino/__init__.py | 0 .../workers/arduino/arduino_control_worker.py | 128 --- .../workers/arduino/arduino_relay_worker.py | 215 ----- .../workers/arduino/arduino_sensor_worker.py | 135 ---- .../mudpi/workers/arduino/arduino_worker.py | 244 ------ build/lib/mudpi/workers/arduino/worker.py | 75 -- build/lib/mudpi/workers/linux/__init__.py | 0 .../lib/mudpi/workers/linux/camera_worker.py | 210 ----- .../lib/mudpi/workers/linux/control_worker.py | 79 -- .../lib/mudpi/workers/linux/display_worker.py | 261 ------ build/lib/mudpi/workers/linux/i2c_worker.py | 88 -- build/lib/mudpi/workers/linux/relay_worker.py | 216 ----- .../lib/mudpi/workers/linux/sensor_worker.py | 100 --- build/lib/mudpi/workers/linux/worker.py | 87 -- build/lib/mudpi/workers/sequence_worker.py | 360 --------- build/lib/mudpi/workers/worker.py | 79 -- 121 files changed, 128 insertions(+), 12452 deletions(-) delete mode 100644 build/lib/mudpi/__init__.py delete mode 100644 build/lib/mudpi/__main__.py delete mode 100644 build/lib/mudpi/config.py delete mode 100644 build/lib/mudpi/constants.py delete mode 100644 build/lib/mudpi/controls/__init__.py delete mode 100644 build/lib/mudpi/controls/arduino/__init__.py delete mode 100644 build/lib/mudpi/controls/arduino/button_control.py delete mode 100644 build/lib/mudpi/controls/arduino/control.py delete mode 100644 build/lib/mudpi/controls/arduino/potentiometer_control.py delete mode 100644 build/lib/mudpi/controls/arduino/switch_control.py delete mode 100644 build/lib/mudpi/controls/linux/__init__.py delete mode 100644 build/lib/mudpi/controls/linux/button_control.py delete mode 100644 build/lib/mudpi/controls/linux/control.py delete mode 100644 build/lib/mudpi/controls/linux/switch_control.py delete mode 100644 build/lib/mudpi/core.py delete mode 100644 build/lib/mudpi/events/__init__.py delete mode 100644 build/lib/mudpi/events/adaptors/__init__.py delete mode 100644 build/lib/mudpi/events/adaptors/mqtt.py delete mode 100644 build/lib/mudpi/events/adaptors/redis.py delete mode 100644 build/lib/mudpi/exceptions.py delete mode 100644 build/lib/mudpi/extensions/__init__.py delete mode 100644 build/lib/mudpi/extensions/action/__init__.py delete mode 100644 build/lib/mudpi/extensions/bme680/__init__.py delete mode 100644 build/lib/mudpi/extensions/bme680/sensor.py delete mode 100644 build/lib/mudpi/extensions/camera/__init__.py delete mode 100644 build/lib/mudpi/extensions/char_display/__init__.py delete mode 100644 build/lib/mudpi/extensions/control/__init__.py delete mode 100644 build/lib/mudpi/extensions/control/trigger.py delete mode 100644 build/lib/mudpi/extensions/cron/__init__.py delete mode 100644 build/lib/mudpi/extensions/cron/trigger.py delete mode 100644 build/lib/mudpi/extensions/dht/__init__.py delete mode 100644 build/lib/mudpi/extensions/dht/sensor.py delete mode 100644 build/lib/mudpi/extensions/example/__init__.py delete mode 100644 build/lib/mudpi/extensions/example/char_display.py delete mode 100644 build/lib/mudpi/extensions/example/control.py delete mode 100644 build/lib/mudpi/extensions/example/sensor.py delete mode 100644 build/lib/mudpi/extensions/example/toggle.py delete mode 100644 build/lib/mudpi/extensions/gpio/__init__.py delete mode 100644 build/lib/mudpi/extensions/gpio/control.py delete mode 100644 build/lib/mudpi/extensions/gpio/sensor.py delete mode 100644 build/lib/mudpi/extensions/gpio/toggle.py delete mode 100644 build/lib/mudpi/extensions/group/__init__.py delete mode 100644 build/lib/mudpi/extensions/group/trigger.py delete mode 100644 build/lib/mudpi/extensions/i2c/__init__.py delete mode 100644 build/lib/mudpi/extensions/i2c/char_display.py delete mode 100644 build/lib/mudpi/extensions/mqtt/__init__.py delete mode 100644 build/lib/mudpi/extensions/mqtt/sensor.py delete mode 100644 build/lib/mudpi/extensions/nanpy/__init__.py delete mode 100644 build/lib/mudpi/extensions/nanpy/char_display.py delete mode 100644 build/lib/mudpi/extensions/nanpy/control.py delete mode 100644 build/lib/mudpi/extensions/nanpy/sensor.py delete mode 100644 build/lib/mudpi/extensions/nanpy/toggle.py delete mode 100644 build/lib/mudpi/extensions/picamera/__init__.py delete mode 100644 build/lib/mudpi/extensions/picamera/camera.py delete mode 100644 build/lib/mudpi/extensions/redis/__init__.py delete mode 100644 build/lib/mudpi/extensions/redis/sensor.py delete mode 100644 build/lib/mudpi/extensions/rtsp/__init__.py delete mode 100644 build/lib/mudpi/extensions/rtsp/camera.py delete mode 100644 build/lib/mudpi/extensions/sensor/__init__.py delete mode 100644 build/lib/mudpi/extensions/sensor/trigger.py delete mode 100644 build/lib/mudpi/extensions/sequence/__init__.py delete mode 100644 build/lib/mudpi/extensions/socket/__init__.py delete mode 100644 build/lib/mudpi/extensions/state/__init__.py delete mode 100644 build/lib/mudpi/extensions/state/trigger.py delete mode 100644 build/lib/mudpi/extensions/sun/__init__.py delete mode 100644 build/lib/mudpi/extensions/sun/sensor.py delete mode 100644 build/lib/mudpi/extensions/t9602/__init__.py delete mode 100644 build/lib/mudpi/extensions/t9602/sensor.py delete mode 100644 build/lib/mudpi/extensions/toggle/__init__.py delete mode 100644 build/lib/mudpi/extensions/toggle/trigger.py delete mode 100644 build/lib/mudpi/extensions/trigger/__init__.py delete mode 100644 build/lib/mudpi/importer.py delete mode 100644 build/lib/mudpi/logger/Logger.py delete mode 100644 build/lib/mudpi/logger/__init__.py delete mode 100644 build/lib/mudpi/managers/__init__.py delete mode 100644 build/lib/mudpi/managers/core_manager.py delete mode 100644 build/lib/mudpi/managers/extension_manager.py delete mode 100644 build/lib/mudpi/managers/state_manager.py delete mode 100644 build/lib/mudpi/mudpi_main.py delete mode 100644 build/lib/mudpi/registry.py delete mode 100644 build/lib/mudpi/sensors/__init__.py delete mode 100644 build/lib/mudpi/sensors/arduino/__init__.py delete mode 100644 build/lib/mudpi/sensors/arduino/float_sensor.py delete mode 100644 build/lib/mudpi/sensors/arduino/humidity_sensor.py delete mode 100644 build/lib/mudpi/sensors/arduino/light_sensor.py delete mode 100644 build/lib/mudpi/sensors/arduino/rain_sensor.py delete mode 100644 build/lib/mudpi/sensors/arduino/sensor.py delete mode 100644 build/lib/mudpi/sensors/arduino/soil_sensor.py delete mode 100644 build/lib/mudpi/sensors/arduino/temperature_sensor.py delete mode 100644 build/lib/mudpi/sensors/base_sensor.py delete mode 100644 build/lib/mudpi/sensors/linux/__init__.py delete mode 100644 build/lib/mudpi/sensors/linux/float_sensor.py delete mode 100644 build/lib/mudpi/sensors/linux/humidity_sensor.py delete mode 100644 build/lib/mudpi/sensors/linux/i2c/__init__.py delete mode 100644 build/lib/mudpi/sensors/linux/i2c/bme680_sensor.py delete mode 100644 build/lib/mudpi/sensors/linux/i2c/sensor.py delete mode 100644 build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py delete mode 100644 build/lib/mudpi/sensors/linux/sensor.py delete mode 100644 build/lib/mudpi/sensors/mcp3xxx/__init__.py delete mode 100644 build/lib/mudpi/sensors/mcp3xxx/sensor.py delete mode 100644 build/lib/mudpi/sensors/mcp3xxx/soil_sensor.py delete mode 100644 build/lib/mudpi/utils.py delete mode 100644 build/lib/mudpi/workers/__init__.py delete mode 100644 build/lib/mudpi/workers/adc_worker.py delete mode 100644 build/lib/mudpi/workers/arduino/__init__.py delete mode 100644 build/lib/mudpi/workers/arduino/arduino_control_worker.py delete mode 100644 build/lib/mudpi/workers/arduino/arduino_relay_worker.py delete mode 100644 build/lib/mudpi/workers/arduino/arduino_sensor_worker.py delete mode 100644 build/lib/mudpi/workers/arduino/arduino_worker.py delete mode 100644 build/lib/mudpi/workers/arduino/worker.py delete mode 100644 build/lib/mudpi/workers/linux/__init__.py delete mode 100644 build/lib/mudpi/workers/linux/camera_worker.py delete mode 100644 build/lib/mudpi/workers/linux/control_worker.py delete mode 100644 build/lib/mudpi/workers/linux/display_worker.py delete mode 100644 build/lib/mudpi/workers/linux/i2c_worker.py delete mode 100644 build/lib/mudpi/workers/linux/relay_worker.py delete mode 100644 build/lib/mudpi/workers/linux/sensor_worker.py delete mode 100644 build/lib/mudpi/workers/linux/worker.py delete mode 100644 build/lib/mudpi/workers/sequence_worker.py delete mode 100644 build/lib/mudpi/workers/worker.py diff --git a/.gitignore b/.gitignore index 89a4c21..5391d87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,110 @@ -node_modules/ -package-lock.json -scripts/ -tests/ -img/ -mudpi.config -*.log -# Python compiled +# Byte-compiled / optimized / DLL files __pycache__/ -*.egg-info +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ -# Virtual Environments +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env .venv env/ @@ -18,6 +113,26 @@ ENV/ env.bak/ venv.bak/ -# IDE configs -.idea/ -.DS_Store +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/build/lib/mudpi/__init__.py b/build/lib/mudpi/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/__main__.py b/build/lib/mudpi/__main__.py deleted file mode 100644 index 362c46d..0000000 --- a/build/lib/mudpi/__main__.py +++ /dev/null @@ -1,204 +0,0 @@ -""" MudPi Main Run File -Author: Eric Davisson (@theDavisson) [EricDavisson.com] -https://mudpi.app - -This is the main entry point for running MudPi. -""" -import os -import sys -import time -import datetime -import argparse -import threading -from mudpi.config import Config -from mudpi import utils, importer -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.managers.core_manager import CoreManager -from mudpi.constants import __version__, PATH_CONFIG, DEFAULT_CONFIG_FILE, \ - FONT_RESET_CURSOR, FONT_RESET, YELLOW_BACK, GREEN_BACK, FONT_GREEN, FONT_RED, FONT_YELLOW, FONT_PADDING -from mudpi.exceptions import ConfigNotFoundError, ConfigFormatError - - -def main(args=None): - """ The main run entry. """ - if args is None: - args = sys.argv[1:] - - arguments = get_arguments() - - ################################### - ### Make a default config file ### - if arguments.make_config: - config_location = os.path.join(os.getcwd(), arguments.config) - print(f"Generating a default config file at {config_location}") - if os.path.exists(config_location) and not arguments.overwrite: - print(f'{FONT_RED}{"File already exists and `--overwrite` was not set."}{FONT_RESET}') - return - Config().save_to_file(config_path) - return - - ####################### - ### Configurations ### - config_path = os.path.abspath(os.path.join(os.getcwd(), arguments.config)) - - print('Loading MudPi Configs \r', end="", flush=True) - - print(chr(27) + "[2J") - display_greeting() - - manager = CoreManager() - manager.load_mudpi_from_config(config_path) - - print(f'{"Loading MudPi Configs ":.<{FONT_PADDING+1}} {FONT_GREEN}Complete{FONT_RESET}') - - ##################### - ### Bootstrapping ### - - if arguments.debug: - print(f'{YELLOW_BACK}DEBUG MODE ENABLED{FONT_RESET}') - manager.mudpi.config.config["mudpi"]["debug"] = True - # print(arguments) #DEBUG - print(f'Config path: {config_path}') #DEBUG - print(f'Current Directory: {os.getcwd()}') - # print(f'Config keys {mudpi.config.keys()}') - time.sleep(1) - - # Logging Module - try: - print('Initializing Logger \r', end='', flush=True) - manager.initialize_logging() - except Exception as e: - print(f'{"Initializing Logger ":.<{FONT_PADDING}} {FONT_RED}Disabled{FONT_RESET}') - - # MudPi Core Systems - manager.load_mudpi_core() - - Logger.log_formatted(LOG_LEVEL["warning"], "Initializing Core ", "Complete", 'success') - - Logger.log(LOG_LEVEL["debug"], f'{" Detecting Configurations ":_^{FONT_PADDING+8}}') - # Load the Extension System - loaded_extensions = manager.load_all_extensions() - - Logger.log_formatted( - LOG_LEVEL["warning"], f"Loaded {len(loaded_extensions)} Extensions ", "Complete", 'success' - ) - - Logger.log_formatted(LOG_LEVEL["warning"], "MudPi Fully Loaded", 'Complete', 'success') - - ######################### - ### Start All Systems ### - Logger.log(LOG_LEVEL["debug"], f'{" Start Systems ":_^{FONT_PADDING+8}}') - Logger.log_formatted(LOG_LEVEL["debug"], "Starting All Workers ", 'Pending', 'notice') - manager.mudpi.start_workers() - Logger.log_formatted(LOG_LEVEL["info"], "Started All Workers ", 'Complete', 'success') - - # Everything should be loaded and running - Logger.log_formatted(LOG_LEVEL["info"], "MudPi Systems ", 'Online', 'success') - print(f'{"":_<{FONT_PADDING+8}}\n') - - """ Debug Mode Dump After System Online """ - if arguments.debug and arguments.dump: - manager.debug_dump(cache_dump=arguments.cache_dump) - time.sleep(1) - - - ############################### - """ MAIN PROGRAM HEARTBEAT """ - manager.mudpi.start() - PROGRAM_RUNNING = True - while PROGRAM_RUNNING: - try: - # Keep messages being processed - manager.mudpi.events.get_message() - current_clock = datetime.datetime.now().replace(microsecond=0) - manager.mudpi.events.publish('clock', {"clock":current_clock.strftime("%m-%d-%Y %H-%M-%S"), - "date":str(current_clock.date()), "time": str(current_clock.time())}) - for i in range(10): - time.sleep(0.1) - manager.mudpi.events.get_message() - except KeyboardInterrupt as error: - PROGRAM_RUNNING = False - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - f"Runtime Error: {error}" - ) - PROGRAM_RUNNING = False - - """ PROGRAM SHUTDOWN """ - print(f'{"":_<{FONT_PADDING+8}}') - Logger.log_formatted( - LOG_LEVEL["info"], - "Stopping All Workers for Shutdown ", 'Pending', 'notice' - ) - manager.mudpi.shutdown() - - Logger.log_formatted( - LOG_LEVEL["info"], - "All MudPi Systems ", 'Offline', 'error' - ) - - -def get_arguments(): - """ Process program arguments at runtime and decide entry """ - - parser = argparse.ArgumentParser( - description="MudPi: Private Automation for the Garden & Home." - ) - parser.add_argument("-v", "--version", action="version", version=__version__) - - parser.add_argument( - "-c", - "--config", - metavar="path_to_config", - default=os.path.join(PATH_CONFIG, DEFAULT_CONFIG_FILE), - help="Set path of the MudPi configuration", - ) - parser.add_argument( - "--debug", action="store_true", help="Start MudPi in forced debug mode" - ) - parser.add_argument( - "--dump", action="store_true", help="Display important system information" - ) - parser.add_argument( - "--cache_dump", action="store_true", help="Display cache when --dump is set" - ) - parser.add_argument( - "--verbose", action="store_true", help="Enable verbose output." - ) - parser.add_argument( - "--make_config", action="store_true", help="Create a default MudPi config file." - ) - - parser.add_argument( - "--overwrite", - action="store_true", - default=False, - help="Overwrite existing config file with [--make_config]." - ) - - arguments = parser.parse_args() - - return arguments - - -def display_greeting(): - """ Print a header with program info for startup """ - greeting = '███╗ ███╗██╗ ██╗██████╗ ██████╗ ██╗\n'\ - '████╗ ████║██║ ██║██╔══██╗██╔══██╗██║\n'\ - '██╔████╔██║██║ ██║██║ ██║██████╔╝██║\n'\ - '██║╚██╔╝██║██║ ██║██║ ██║██╔═══╝ ██║\n'\ - '██║ ╚═╝ ██║╚██████╔╝██████╔╝██║ ██║\n'\ - '╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝' - print(FONT_GREEN) - print(greeting) - print(f'{"":_<{FONT_PADDING+8}}') - print('') - print('Eric Davisson @MudPiApp') - print('https://mudpi.app') - print('Version: ', __version__) - print(FONT_RESET) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/build/lib/mudpi/config.py b/build/lib/mudpi/config.py deleted file mode 100644 index a3fcc57..0000000 --- a/build/lib/mudpi/config.py +++ /dev/null @@ -1,201 +0,0 @@ -import os -import json -import yaml -from mudpi.constants import (FONT_YELLOW, RED_BACK, FONT_RESET, IMPERIAL_SYSTEM, PATH_MUDPI, PATH_CONFIG, DEFAULT_CONFIG_FILE) -from mudpi.exceptions import ConfigNotFoundError, ConfigError - - -class Config(object): - """ MudPi Config Class - - A class to represent the MudPi configuration that - is typically pulled from a file. - """ - def __init__(self, config_path=None): - self.config_path = config_path or os.path.abspath(os.path.join(os.getcwd(), PATH_CONFIG)) - - self.config = {} - self.set_defaults() - - - """ Properties """ - @property - def name(self): - return self.config.get('mudpi', {}).get('name', 'MudPi') - - @name.setter - def name(self, value): - self.config.get('mudpi', {})['name'] = value - - @property - def debug(self): - return self.config.get('mudpi', {}).get('debug', False) - - @debug.setter - def debug(self, value): - self.config.setdefault('mudpi', {})['debug'] = value - - - """ Methods """ - def path(self, *path): - """ Returns path relative to the config folder. """ - return os.path.join(self.config_path, *path) - - def set_defaults(self): - """ Set default configurations for any null values """ - core_config = { - "name": self.config.get('mudpi', {}).get('name', 'MudPi'), - "debug": self.config.get('mudpi', {}).get('debug', False), - "unit_system": self.config.get('mudpi', {}).get('unit_system', "imperial"), - } - self.config['mudpi'] = core_config - - def to_dict(self): - """ Return Config as Dict """ - return dict(self.config) - - def to_json(self): - """ Return Config as JSON """ - return json.dumps(self.to_dict()) - - def keys(self): - """ Return the keys of the config """ - return self.config.keys() - - def values(self): - """ Return the values of the config """ - return self.config.values() - - def setdefault(self, key, default): - """ Provide setdefault on config """ - return self.config.setdefault(key, default) - - def file_exists(self, file=None): - """ Check if config files exists at given path """ - file = file or self.path(DEFAULT_CONFIG_FILE) - return os.path.exists(file) - - def load_from_file(self, file=None, format=None): - """ - Load configurations from a file. - Format: 'JSON' or 'YAML' - """ - - if file is not None: - file = self.validate_file(file) - else: - file = self.path(DEFAULT_CONFIG_FILE) - - if format is None: - format = self.config_format(file) - - try: - with open(file) as f: - config = f.read() - f.close() - if format is not None: - if format.lower() == 'json': - config = self.load_from_json(config) - elif format.lower() == 'yaml': - config = self.load_from_yaml(config) - else: - config = dict(config) - if config: - self.config = config - self.config_path = os.path.split(file)[0] - return config - except FileNotFoundError: - print( - f'{RED_BACK}There is no configuration file found on the ' - f'filesystem{FONT_RESET}\n\r' - ) - raise ConfigNotFoundError() - except Exception as e: - print( - f'{RED_BACK}There was an error loading config file. {FONT_RESET}\n\r' - ) - raise ConfigError() - - def load_from_json(self, json_data): - """ Load configs from JSON """ - try: - self.config = json.loads(json_data) - return self.config - except Exception as e: - print(f'{RED_BACK}Problem loading configs from JSON {FONT_RESET}\n{FONT_YELLOW}{e}{FONT_RESET}\r') - - def load_from_yaml(self, yaml_data): - """ Load configs from YAML """ - try: - self.config = yaml.load(yaml_data, yaml.FullLoader) - return self.config - except Exception as e: - print(f'{RED_BACK}Problem loading configs from YAML {FONT_RESET}\n{FONT_YELLOW}{e}{FONT_RESET}\r') - - def save_to_file(self, file=None, format=None, config=None): - """ Save current configs to a file - File: Full path to file - Format: 'json' or 'yaml' - Config: Dict of data to write to file (Default: self) - """ - if file is not None: - file = self.validate_file(file) - else: - file = self.path(DEFAULT_CONFIG_FILE) - - if format is None: - format = self.config_format(file) - - config = config or self.config - if format == 'json': - config = json.dumps(config, sort_keys=True, indent=4) - elif format == 'yaml': - config = yaml.dump(config) - else: - config = str(config) - with open(file, 'w') as f: - f.write(config) - return True - - def validate_file(self, file): - """ Validate a file path and return a prepared path to save """ - if '.' in file: - if not self.file_exists(file): - raise ConfigNotFoundError(f"The config path {file} does not exist.") - return False - extensions = ['.config', '.json', '.yaml', '.conf'] - if not any([file.endswith(extension) for extension in extensions]): - raise ConfigFormatError("An unknown config file format was provided in the config path.") - return False - else: - # Path provided but not file - file = os.path.join(file, DEFAULT_CONFIG_FILE) - return file - - def config_format(self, file): - """ Returns the file format if supported """ - if '.' in file: - if any(extension in file for extension in ['.config', '.json', '.conf']): - config_format = 'json' - elif '.yaml' in file: - config_format = 'yaml' - else: - config_format = None - - return config_format - - def get(self, key, default=None, replace_char=None): - """ Get an item from the config with a default - Use replace_char to slug the config value - """ - value = self.config.get(key, default) - - if replace_char: - if type(value) == str: - value = value.replace(" ", replace_char).lower() - - return value - - def __repr__(self): - """ Debug print of config """ - return f'' \ No newline at end of file diff --git a/build/lib/mudpi/constants.py b/build/lib/mudpi/constants.py deleted file mode 100644 index 59a11e5..0000000 --- a/build/lib/mudpi/constants.py +++ /dev/null @@ -1,142 +0,0 @@ -import os - -""" Constants used by MudPi """ -MAJOR_VERSION = 0 -MINOR_VERSION = 10 -PATCH_VERSION = "0" -__version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}' - -""" PATHS """ -DEFAULT_CONFIG_FILE = "mudpi.config" -PATH_MUDPI = os.getcwd() # /etc/mudpi -PATH_CORE = f"{PATH_MUDPI}/core/mudpi" -PATH_LOGS = f"{PATH_MUDPI}/logs" -PATH_CONFIG = f"{PATH_CORE}" - -""" DEFAULTS """ -DEFAULT_UPDATE_INTERVAL = 30 - -""" DATES / TIMES """ -MONTHS = { - 'jan': 'January', - 'feb': 'February', - 'mar': 'March', - 'apr': 'April', - 'may': 'May', - 'jun': 'June', - 'jul': 'July', - 'aug': 'August', - 'sep': 'September', - 'oct': 'October', - 'nov': 'November', - 'dec': 'December' } -WEEKDAYS = { - "mon": 'Monday', - "tue": 'Tuesday', - "wed": 'Wednesday', - "thu": 'Thursday', - "fri": 'Friday', - "sat": 'Saturday', - "sun": 'Sunday' } - - -#### Display Characters ##### -FONT_RESET_CURSOR = "\x1b[1F" -FONT_RED = "\033[1;31m" -FONT_GREEN = '\033[1;32m' -FONT_YELLOW = "\033[1;33m" -FONT_PURPLE = "\033[1;34m" -FONT_MAGENTA = "\033[1;35m" -FONT_CYAN = "\033[1;36m" -FONT_RESET = "\x1b[0m" -RED_BACK = "\x1b[41;37m" -GREEN_BACK = "\x1b[42;30m" -YELLOW_BACK = "\x1b[43;30m" -FONT_PADDING = 52 - - -""" COMPONENT CLASSIFIERS """ -CLASSIFIER_BATTERY = "battery" -CLASSIFIER_CURRENT = "current" -CLASSIFIER_EC = "electrical_conductivity" -CLASSIFIER_ENERGY = "energy" -CLASSIFIER_FLOWMETER = "flowmeter" -CLASSIFIER_HUMIDITY = "humidity" -CLASSIFIER_ILLUMINANCE = "illuminance" -CLASSIFIER_SIGNAL_STRENGTH = "signal_strength" -CLASSIFIER_TEMPERATURE = "temperature" -CLASSIFIER_TIMESTAMP = "timestamp" -CLASSIFIER_MOISTURE = "moisture" -CLASSIFIER_PH = "ph" -CLASSIFIER_PRESSURE = "pressure" -CLASSIFIER_POWER = "power" -CLASSIFIER_POWER_FACTOR = "power_factor" -CLASSIFIER_VOLTAGE = "voltage" -CLASSIFIERS = [ - CLASSIFIER_BATTERY, - CLASSIFIER_CURRENT, - CLASSIFIER_EC, - CLASSIFIER_ENERGY, - CLASSIFIER_FLOWMETER, - CLASSIFIER_HUMIDITY, - CLASSIFIER_ILLUMINANCE, - CLASSIFIER_SIGNAL_STRENGTH, - CLASSIFIER_TEMPERATURE, - CLASSIFIER_TIMESTAMP, - CLASSIFIER_MOISTURE, - CLASSIFIER_PH, - CLASSIFIER_PRESSURE, - CLASSIFIER_POWER, - CLASSIFIER_POWER_FACTOR, - CLASSIFIER_VOLTAGE -] - -""" UNITS OF MEASUREMENT """ -IMPERIAL_SYSTEM = 1 -METRIC_SYSTEM = 2 - -# Degree units -DEGREE = "°" - -# Temperature units -TEMP_CELSIUS = f"{DEGREE}C" -TEMP_FAHRENHEIT = f"{DEGREE}F" - -# Conductivity units -CONDUCTIVITY = f"µS" - -# Percentage units -PERCENTAGE = "%" - - -# #### API / SOCKET #### -SOCKET_PORT = 7002 -SPROUT_PORT = 7003 -WS_PORT = 7004 -SERVER_PORT = 8080 - -URL_ROOT = "/" -URL_API = "/api/" -URL_API_CONFIG = "/api/config" - -HTTP_OK = 200 -HTTP_CREATED = 201 -HTTP_ACCEPTED = 202 -HTTP_MOVED_PERMANENTLY = 301 -HTTP_BAD_REQUEST = 400 -HTTP_UNAUTHORIZED = 401 -HTTP_FORBIDDEN = 403 -HTTP_NOT_FOUND = 404 -HTTP_METHOD_NOT_ALLOWED = 405 -HTTP_UNPROCESSABLE_ENTITY = 422 -HTTP_TOO_MANY_REQUESTS = 429 -HTTP_INTERNAL_SERVER_ERROR = 500 -HTTP_BAD_GATEWAY = 502 -HTTP_SERVICE_UNAVAILABLE = 503 - -HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" - -CONTENT_TYPE_JSON = "application/json" -CONTENT_TYPE_MULTIPART = "multipart/x-mixed-replace; boundary={}" -CONTENT_TYPE_TEXT_PLAIN = "text/plain" - diff --git a/build/lib/mudpi/controls/__init__.py b/build/lib/mudpi/controls/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/controls/arduino/__init__.py b/build/lib/mudpi/controls/arduino/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/controls/arduino/button_control.py b/build/lib/mudpi/controls/arduino/button_control.py deleted file mode 100644 index c0cab0d..0000000 --- a/build/lib/mudpi/controls/arduino/button_control.py +++ /dev/null @@ -1,39 +0,0 @@ -import time -import datetime -import json -import redis -from .control import Control -from nanpy import (ArduinoApi, SerialManager) - -default_connection = SerialManager(device='/dev/ttyUSB0') -# r = redis.Redis(host='127.0.0.1', port=6379) - -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_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 - - self.previous_state = state - return state - - def read_raw(self): - return super().read() diff --git a/build/lib/mudpi/controls/arduino/control.py b/build/lib/mudpi/controls/arduino/control.py deleted file mode 100644 index 762dec8..0000000 --- a/build/lib/mudpi/controls/arduino/control.py +++ /dev/null @@ -1,61 +0,0 @@ -import time -import json -import redis -from nanpy import (ArduinoApi, SerialManager) -import sys - - -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.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/build/lib/mudpi/controls/arduino/potentiometer_control.py b/build/lib/mudpi/controls/arduino/potentiometer_control.py deleted file mode 100644 index dbf6d05..0000000 --- a/build/lib/mudpi/controls/arduino/potentiometer_control.py +++ /dev/null @@ -1,38 +0,0 @@ -import time -import datetime -import json -import redis -from .control import Control -from nanpy import (ArduinoApi, SerialManager) - -default_connection = SerialManager(device='/dev/ttyUSB0') -# r = redis.Redis(host='127.0.0.1', port=6379) - -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 read_raw(self): - return super().read() - diff --git a/build/lib/mudpi/controls/arduino/switch_control.py b/build/lib/mudpi/controls/arduino/switch_control.py deleted file mode 100644 index 2b4eb45..0000000 --- a/build/lib/mudpi/controls/arduino/switch_control.py +++ /dev/null @@ -1,40 +0,0 @@ -import time -import datetime -import json -import redis -from .control import Control -from nanpy import (ArduinoApi, SerialManager) - -default_connection = SerialManager(device='/dev/ttyUSB0') -# r = redis.Redis(host='127.0.0.1', port=6379) - -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_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 - - self.previous_state = state - return state - - def read_raw(self): - return super().read() diff --git a/build/lib/mudpi/controls/linux/__init__.py b/build/lib/mudpi/controls/linux/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/controls/linux/button_control.py b/build/lib/mudpi/controls/linux/button_control.py deleted file mode 100644 index 0890168..0000000 --- a/build/lib/mudpi/controls/linux/button_control.py +++ /dev/null @@ -1,30 +0,0 @@ -import time -import json -import redis -import datetime -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/build/lib/mudpi/controls/linux/control.py b/build/lib/mudpi/controls/linux/control.py deleted file mode 100644 index e895d95..0000000 --- a/build/lib/mudpi/controls/linux/control.py +++ /dev/null @@ -1,107 +0,0 @@ -import time -import json -import redis -import sys - -try: - import board - import digitalio - from adafruit_debouncer import Debouncer - SUPPORTED_DEVICE = True -except: - SUPPORTED_DEVICE = False - - - - -# 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): - 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 - - if SUPPORTED_DEVICE: - self.pin_obj = getattr(board, pin) - self.gpio = digitalio - self.debounce = debounce if debounce is not None else None - - 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 - - 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 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""" - if SUPPORTED_DEVICE: - 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 SUPPORTED_DEVICE: - 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) - 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""" - if SUPPORTED_DEVICE: - data = self.gpio.DigitalInOut(self.pin_obj).value - return data - return {} - - def emitEvent(self, data): - message = { - 'event': 'ControlUpdate', - 'data': { - self.key: data - } - } - print(message["data"]) - self.r.publish('controls', json.dumps(message)) diff --git a/build/lib/mudpi/controls/linux/switch_control.py b/build/lib/mudpi/controls/linux/switch_control.py deleted file mode 100644 index 82f155f..0000000 --- a/build/lib/mudpi/controls/linux/switch_control.py +++ /dev/null @@ -1,42 +0,0 @@ -import time -import json -import redis -import datetime -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/build/lib/mudpi/core.py b/build/lib/mudpi/core.py deleted file mode 100644 index 0c0bc19..0000000 --- a/build/lib/mudpi/core.py +++ /dev/null @@ -1,236 +0,0 @@ -import os -import enum -import time -import threading - -from mudpi import importer -from mudpi.config import Config -from mudpi.events import EventSystem -from mudpi.constants import DEFAULT_CONFIG_FILE -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.managers.state_manager import StateManager -from mudpi.exceptions import ConfigNotFoundError, ConfigFormatError -from mudpi.registry import Registry, ActionRegistry, ComponentRegistry - -class MudPi: - """ - MudPi Core Class - - The core class of MudPi that holds all the important items - to run the MudPi system. - """ - def __init__(self, config_path=None): - self.config_path = config_path - self.config = None - - self.state = CoreState.not_running - - # Main data storage between extensions - # eventually change this to helper and backup in redis - self.cache = {} - - self.threads = {} - self.thread_events = { - # Event to signal system to shutdown - 'mudpi_running': threading.Event(), - # Event to tell workers to begin working - "core_running": threading.Event() - } - - # Setup the registries - self.components = ComponentRegistry(self, 'components') - self.extensions = Registry(self, 'extensions') - self.workers = Registry(self, 'workers') - self.actions = ActionRegistry(self, 'actions') - - # System is Running - self.thread_events['mudpi_running'].set() - time.sleep(0.1) - - """ Properties """ - @property - def is_prepared(self): - """ Return if MudPi is prepared """ - return self.thread_events['mudpi_running'].is_set() - - @property - def is_loaded(self): - """ Return if MudPi is loaded or previously was. """ - return self.state in (CoreState.loaded, CoreState.stopped, CoreState.running) - - @property - def is_loading(self): - """ Return if MudPi is loaded or previously was. """ - return self.state in (CoreState.loading,) - - @property - def is_running(self): - """ Return if MudPi is running. """ - return self.state in (CoreState.running,) - - @property - def is_stopping(self): - """ Return if MudPi is stopping. """ - return self.state in (CoreState.stopping,) - - - """ Methods """ - def load_config(self, config_path=None): - """ Load MudPi Configurations """ - if self.state == CoreState.preparing: - """ Already loading """ - return - - self.state = CoreState.preparing - - config_path = config_path or self.config_path - - self.config = Config(config_path=config_path) - - if self.config.load_from_file(config_path): - if config_path and config_path != self.config_path: - self.config_path = config_path - - self.state = CoreState.prepared - return True - return False - - def load_core(self): - """ Init the MudPi Core - - Load the State Machine - - Prepare Event Bus (Drivers) - - Prepare the Threads / Managers - - Register Core Actions - """ - if self.config is None: - # Error Config not loaded call `load_config()` first - return - - if self.is_running: - # Error core already running - return - - if self.state != CoreState.prepared: - # Core is not waiting to load likely already loaded - return - - if self.state == CoreState.loading: - # Error core is already loading - return - - self.state = CoreState.loading - - self.states = StateManager(self, self.config.get('mudpi', {}).get('events', {}).get('redis')) - - self.events = EventSystem(self.config.get('mudpi', {}).get('events', {})) - self.events.connect() - self.events.subscribe('action_call', self.actions.handle_call) - - self.actions.register('turn_on', self.start, 'mudpi') - self.actions.register('turn_off', self.stop, namespace='mudpi') - self.actions.register('shutdown', self.shutdown, namespace='mudpi') - - self.state = CoreState.loaded - self.events.publish('core', {'event': 'Loaded'}) - return True - - def start(self, data=None): - """ Signal MudPi to Start Once Loaded """ - if self.config is None: - # Error Config not loaded call `load_config()` first - return - - if self.is_running: - # Error core already running - return - - if not self.is_loaded: - # Error core not loaded call `load_core()` first - return - - - self.events.publish('core', {'event': 'Starting'}) - self.state = CoreState.starting - self.thread_events['core_running'].set() - self.state = CoreState.running - self.events.publish('core', {'event': 'Started'}) - return True - - def stop(self, data=None): - """ Signal MudPi to Stop but Not UnLoad """ - self.events.publish('core', {'event': 'Stopping'}) - self.state = CoreState.stopping - self.thread_events['core_running'].clear() - self.state = CoreState.stopped - self.events.publish('core', {'event': 'Stopped'}) - return True - - def shutdown(self, data=None): - """ Signal MudPi to Stop and Unload """ - self.stop() - self.events.publish('core', {'event': 'ShuttingDown'}) - self.unload_extensions() - self.thread_events['mudpi_running'].clear() - self.state = CoreState.not_running - - _closed_threads = [] - # First pass of threads to find out which ones are slower - for thread_name, thread in self.threads.items(): - thread.join(5) - if not thread.is_alive(): - _closed_threads.append(thread_name) - else: - Logger.log_formatted(LOG_LEVEL["warning"], - f"Worker {thread_name} is Still Stopping ", "Delayed", "notice") - - for _thread in _closed_threads: - del self.threads[_thread] - - # Now attempt to clean close slow threads - _closed_threads = [] - for thread_name, thread in self.threads.items(): - thread.join(20) - if not thread.is_alive(): - _closed_threads.append(thread_name) - else: - Logger.log_formatted(LOG_LEVEL["error"], - f"Worker {thread_name} Failed to Shutdown ", "Not Responding", "error") - - - self.events.publish('core', {'event': 'Shutdown'}) - self.events.disconnect() - return True - - def start_workers(self): - """ Start Workers and Create Threads """ - for key, worker in self.workers.items(): - _thread = worker.run() - self.threads[key] = _thread - return True - - def reload_workers(self): - """ Reload Workers and Configurations """ - pass - - def unload_extensions(self): - """ Cleanup all extensions for shutdown or restart """ - for key, extension in self.extensions.items(): - extension.unload() - return True - - -class CoreState(enum.Enum): - """ Enums for the current state of MudPi. """ - - not_running = "NOT_RUNNING" # New MudPi instance - preparing = "PREPARING" # loading configs - prepared = "PREPARED" # configs loaded waiting to load core - loading = "LOADING" # core is loading - loaded = "LOADED" # core is loaded - starting = "STARTING" # core is starting - running = "RUNNING" # Core loaded and everying running - stopping = "STOPPING" # waing to shutdown - stopped = "STOPPED" # loaded but stopped (previously running) - - def __str__(self): - return self.value \ No newline at end of file diff --git a/build/lib/mudpi/events/__init__.py b/build/lib/mudpi/events/__init__.py deleted file mode 100644 index 7552e43..0000000 --- a/build/lib/mudpi/events/__init__.py +++ /dev/null @@ -1,84 +0,0 @@ -""" The core Event System for MudPi. - - Uses adaptors to provide events across - different protocols for internal communications. - - Available Adaptors: 'mqtt', 'redis' - Default: redis -""" -from mudpi.events import adaptors -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class EventSystem: - """ Main event manager that loads adaptors - and coordinates the bus operations. """ - - def __init__(self, config={}): - self.config = config - self.prefix = config.get('prefix', 'mudpi_core_') - self.topics = {} - self.adaptors = {} - self._load_adaptors() - - def _load_adaptors(self): - for key, config in self.config.items(): - if key in adaptors.Adaptor.adaptors: - self.adaptors[key] = adaptors.Adaptor.adaptors[key](config) - self.topics[key] = [] - - def connect(self): - connection_data = {} - for key, adaptor in self.adaptors.items(): - Logger.log_formatted( - LOG_LEVEL["debug"], - f"Preparing Event System for {key} ", 'Pending', 'notice' - ) - connection_data[key] = adaptor.connect() - Logger.log_formatted( - LOG_LEVEL["info"], - f"Event System Ready on {key} ", 'Connected', 'success' - ) - return connection_data - - def disconnect(self): - for key, adaptor in self.adaptors.items(): - adaptor.disconnect() - return True - - def subscribe(self, topic, callback): - """ Add a subscriber to an event """ - for key, adaptor in self.adaptors.items(): - adaptor.subscribe(topic, callback) - self.topics[key].append(topic) - return True - - def unsubscribe(self, topic): - """ Remove a subscriber from an event """ - for key, adaptor in self.adaptors.items(): - adaptor.unsubscribe(topic) - self.topics[key].remove(topic) - return True - - def publish(self, topic, data=None): - """ Publish an event on an topic """ - for key, adaptor in self.adaptors.items(): - adaptor.publish(topic, data) - return True - - def subscribe_once(self, topic, callback): - """ Listen to an event once """ - for key, adaptor in self.adaptors.items(): - adaptor.subscribe_once(topic, callback) - return True - - def get_message(self): - """ Request any new messages because some protocols - require a poll for data """ - for key, adaptor in self.adaptors.items(): - adaptor.get_message() - - def events(self): - """ Return all the events subscribed to [List] """ - return self.topics - diff --git a/build/lib/mudpi/events/adaptors/__init__.py b/build/lib/mudpi/events/adaptors/__init__.py deleted file mode 100644 index be3d783..0000000 --- a/build/lib/mudpi/events/adaptors/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -class Adaptor: - """ Base adaptor for pubsub event system """ - - # This key should represent key in configs that it will load form - key = None - - adaptors = {} - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - cls.adaptors[cls.key] = cls - - def __init__(self, config={}): - self.config = config - - def connect(self): - """ Authenticate to system and cache connections """ - raise NotImplementedError() - - def disconnect(self): - """ Close active connections and cleanup subscribers """ - raise NotImplementedError() - - def subscribe(self, topic, callback): - """ Listen on a topic and pass event data to callback """ - raise NotImplementedError() - - def unsubscribe(self, topic): - """ Stop listening for events on a topic """ - raise NotImplementedError() - - def publish(self, topic, data=None): - """ Publish an event on the topic """ - raise NotImplementedError() - - """ No need to override this unless necessary """ - def subscribe_once(self, topic, callback): - """ Subscribe to topic for only one event """ - def handle_once(data): - """ Wrapper to unsubscribe after event handled """ - self.unsubscribe(topic) - if callable(callback): - # Pass data to real callback - callback(data) - - return self.subscribe(topic, handle_once) - - def get_message(self): - """ Some protocols need to initate a poll for new messages """ - pass - -# Import adaptors -from . import redis, mqtt - diff --git a/build/lib/mudpi/events/adaptors/mqtt.py b/build/lib/mudpi/events/adaptors/mqtt.py deleted file mode 100644 index b42f1a3..0000000 --- a/build/lib/mudpi/events/adaptors/mqtt.py +++ /dev/null @@ -1,74 +0,0 @@ -import json -import time -import random -import paho.mqtt.client as mqtt - -from . import Adaptor - - -class MQTTAdaptor(Adaptor): - """ Provide pubsub events over MQTT """ - key = 'mqtt' - - connected = False - loop_started = False - callbacks = {} - - def connect(self): - """ Make mqtt connection and setup broker """ - - def on_conn(client, userdata, flags, rc): - if rc == 0: - self.connected = True - - host = self.config.get('host', "localhost") - # port = self.config.get('port', 1883) - # TODO: Add authentication support - self.connection = mqtt.Client(f'mudpi-{random.randint(0, 100)}') - self.connection.on_connect = on_conn - self.connection.connect(host) - while not self.connected: - self.get_message() - time.sleep(0.1) - return True - - def disconnect(self): - """ Close active connections and cleanup subscribers """ - self.connection.loop_stop() - self.connection.disconnect() - return True - - def subscribe(self, topic, callback): - """ Listen on a topic and pass event data to callback """ - if topic not in self.callbacks: - self.callbacks[topic] = [callback] - else: - if callback not in self.callbacks[topic]: - self.callbacks[topic].append(callback) - - def callback_handler(client, userdata, message): - # log = f"{message.payload.decode()} {message.topic}" - if message.topic in self.callbacks: - for callbk in self.callbacks[message.topic]: - callbk(message.payload) - - self.connection.on_message = callback_handler - return self.connection.subscribe(topic) - - def unsubscribe(self, topic): - """ Stop listening for events on a topic """ - del self.callbacks[topic] - return self.connection.unsubscribe(topic) - - def publish(self, topic, data=None): - """ Publish an event on the topic """ - if data: - return self.connection.publish(topic, json.dumps(data)) - - return self.connection.publish(topic) - - def get_message(self): - """ Check for new messages waiting """ - if not self.loop_started: - self.connection.loop_start() - self.loop_started = True \ No newline at end of file diff --git a/build/lib/mudpi/events/adaptors/redis.py b/build/lib/mudpi/events/adaptors/redis.py deleted file mode 100644 index 23e558e..0000000 --- a/build/lib/mudpi/events/adaptors/redis.py +++ /dev/null @@ -1,55 +0,0 @@ -import redis -import json -from . import Adaptor - - -class RedisAdaptor(Adaptor): - """ Allow MudPi events on Pubsub through Redis """ - key = 'redis' - callbacks = {} - - def connect(self): - """ Make redis connection and setup pubsub """ - host = self.config.get('host', '127.0.0.1') - port = self.config.get('port', 6379) - self.connection = redis.Redis(host=host, port=port) - self.pubsub = self.connection.pubsub() - return True - - def disconnect(self): - """ Close active connections and cleanup subscribers """ - self.pubsub.close() - self.connection.close() - return True - - def subscribe(self, topic, callback): - """ Listen on a topic and pass event data to callback """ - if topic not in self.callbacks: - self.callbacks[topic] = [callback] - else: - if callback not in self.callbacks[topic]: - self.callbacks[topic].append(callback) - - def callback_handler(message): - """ callback handler to allow multiple hanlders on one topic """ - if message["channel"] in self.callbacks: - for callbk in self.callbacks[message["channel"]]: - callbk(message) - - return self.pubsub.subscribe(**{topic: callback_handler}) - - def unsubscribe(self, topic): - """ Stop listening for events on a topic """ - del self.callbacks[topic] - return self.pubsub.unsubscribe(topic) - - def publish(self, topic, data=None): - """ Publish an event on the topic """ - if data: - return self.connection.publish(topic, json.dumps(data)) - - return self.connection.publish(topic) - - def get_message(self): - """ Check for new messages waiting """ - return self.pubsub.get_message() \ No newline at end of file diff --git a/build/lib/mudpi/exceptions.py b/build/lib/mudpi/exceptions.py deleted file mode 100644 index 5c313e7..0000000 --- a/build/lib/mudpi/exceptions.py +++ /dev/null @@ -1,41 +0,0 @@ -""" MudPi System Exceptions -All the custom exceptions for MudPi. -""" - -class MudPiError(Exception): - """ General MudPi exception occurred. """ - - -""" Config Errors """ -class ConfigError(MudPiError): - """ General error with configurations. """ - -class NoKeyProvidedError(ConfigError): - """ When no config key is specified. """ - -class ConfigFormatError(ConfigError): - """ Error with configuration formatting. """ - -class ConfigNotFoundError(ConfigError): - """ Error with no config file found. """ - - -""" State Errors """ -class InvalidStateError(MudPiError): - """ When a problem occurs with impromper state machine states. """ - - -""" Extension Errors """ -class ExtensionNotFound(MudPiError): - """ Error when problem importing extensions """ - def __init__(self, extension): - super().__init__(f"Extension '{extension}' not found.") - self.extension = extension - -class RecursiveDependency(MudPiError): - """ Error when extension references anther extension in dependency loop """ - - def __init__(self, extension, dependency): - super().__init__(f'Recursive dependency loop: {extension} -> {dependency}.') - self.extension = extension - self.dependency = dependency \ No newline at end of file diff --git a/build/lib/mudpi/extensions/__init__.py b/build/lib/mudpi/extensions/__init__.py deleted file mode 100644 index f0dded6..0000000 --- a/build/lib/mudpi/extensions/__init__.py +++ /dev/null @@ -1,335 +0,0 @@ -""" - MudPi Components and Extensions Provided by Core - Extensions can interact with events and add - components to MudPi through interfaces. -""" -import inspect -from mudpi.workers import Worker -from mudpi.exceptions import MudPiError -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.constants import DEFAULT_UPDATE_INTERVAL -from mudpi.managers.extension_manager import ExtensionManager - - -class BaseExtension: - """ Base class of all MudPi Extensions. - Extensions will contain components with interfaces. - The extension is resposible for its own config setup. - """ - registered_extensions = {} - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - if cls.namespace: - cls.registered_extensions[cls.namespace] = cls - - # A unique string slug for the extension - namespace = None - - # Time between component updates. Can be changed via config - update_interval = None # DEFAULT_UPDATE_INTERVAL - - def __init__(self, mudpi): - """ DO NOT OVERRIDE this method. Use init() instead """ - self.mudpi = mudpi - self.config = None - - if self.namespace is not None: - self.load_manager() - - """ Overrideable Methods to Extend """ - def init(self, config): - """ Initialize the extension. Override this method to perform - additional setup the extension may need. Make sure to return - a boolean and assign the config to the extension. - """ - self.config = config - return True - - def validate(self, config): - """ Validate the config for the extension. - Returns valid config or raises a ConfigError - This gets called before `init()`. - """ - return config - - def unload(self): - """ Cleanup any data in memory and release - system resources. This is called to - just before removing the extension. - """ - pass - - - """ Lifecycle Function Hooks """ - def extension_imported(self, *args, **kwargs): - """ Will be called after extension import completes - args: {importer} - """ - pass - - def extension_initialized(self, *args, **kwargs): - """ Will be called after extension `init()` completes - args: {importer, validated_config} - """ - pass - - def extension_registered(self, *args, **kwargs): - """ Will be called after extension added to MudPi """ - pass - - def extension_removed(self, *args, **kwargs): - """ Will be called before extension is removed from MudPi """ - pass - - - """ INTERNAL METHODS: DO NOT OVERRIDE! - These methods and properties are used internally. - """ - def load_manager(self): - """ Initalize a Manager for the Extension """ - self.manager = ExtensionManager(self) - - def __repr__(self): - """ Debug display of extension. """ - return f'' - - -class BaseInterface: - """ Base class of all MudPi Interfaces. - Interfaces connect to components to manage, - monitor and collect data for the extension. - """ - - """ Time between component updates. Can be changed via config""" - update_interval = None # DEFAULT_UPDATE_INTERVAL - - def __init__(self, mudpi, namespace, interface_name, update_interval=None): - """ DO NOT OVERRIDE this method. Use load() instead """ - self.mudpi = mudpi - self.namespace = namespace - self.type = interface_name - self.update_interval = update_interval or self.update_interval - - self.extension = None - self.config = None - self.worker = None - - """ Overrideable Methods to Extend """ - def load(self, config): - """ This will be called per config entry that - references the interface. Process the config - to add components, listen for events, etc. - """ - return True - - def validate(self, config): - """ Validate the config for the interface. - Returns valid config or raises a ConfigError - This gets called before `load()`. - """ - return config - - - """ INTERNAL METHODS: DO NOT OVERRIDE! - These methods and properties are used internally. - """ - @property - def key(self): - """ Create a composite key """ - return f"{self.namespace}.{self.type}.{self.update_interval}" - - def add_component(self, component): - """ Add a component for the interface """ - if not _is_component(component): - raise MudPiError(f"Passed non-component to add_component for {self.namesapce}.") - - # Worker is loaded here to prevent empty workers without components - if self.worker is None: - self.worker = self.load_worker() - - try: - if component.id is None: - Logger.log( - LOG_LEVEL["debug"], f"Interface {self.namespace}:{self.type} component did not define `id`." - ) - return False - if component.id in self.worker.components: - Logger.log( - LOG_LEVEL["debug"], f"Interface {self.namespace}:{self.type} component id ({component.id}) already registered." - ) - return False - self.worker.components[component.id] = self.mudpi.components.register(component.id, component, self.namespace) - component.component_registered(mudpi=self.mudpi, interface=self) - return True - except Exception as error: - Logger.log( - LOG_LEVEL["debug"], f"Interface {self.namespace}:{self.type} unknown error adding component.\n{error}" - ) - - def load_worker(self): - """ Load a worker for any interface components """ - return Worker(self.mudpi, {'key': self.key, 'update_interval': self.update_interval}) - - def __repr__(self): - """ Debug display of extension. """ - return f'' - - -class Component: - """ Base class of all extension components in MudPi - - Components will be dynamically loaded by extensions - based on configs. A component will either be updating - state from device or listening / hanlding events. - """ - - """ Main MudPi Core Instance """ - mudpi = None - - """ Configuration dict passed in at init() """ - config = {} - - """ Set to true after the component completes `init()` """ - setup_complete = False - - """ Time it takes to complete one cycle, used to find slow components """ - processing_time = None - - """ Static Variable to Track Components """ - registered_components = {} - - """ Variable suggestion to use for state """ - _state = None - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - cls.registered_components[cls.__name__] = cls - - - """ Base Constructor """ - def __init__(self, mudpi, config={}): - """ Generally you shouldn't need to override this constructor, - use init() instead. If you do override call `super().__init__()` - """ - self.mudpi = mudpi - self.config = config - self.init() - - """ Properties - Override these depending on desired component functionality - """ - @property - def id(self): - """ Return a unique id for the component """ - return None - - @property - def name(self): - """ Return the display name of the component """ - return None - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return None - - @property - def metadata(self): - """ Returns a dict of additonal info about the component - This is mainly used by the dashboard / frontend - """ - return {} - - @property - def available(self): - """ Return if the component is available """ - return True - - @property - def should_update(self): - """ Boolean if component `update()` will be called each cycle """ - return True - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return None - - - """ Methods """ - def init(self): - """ Called at end if __init__ and - used for additional setup tasks - """ - pass - - def update(self): - """ Get data, run tasks, update state, called during - each work cycle. Don't block longer than update_interfal - to avoid being flagged as a slow component. """ - pass - - def reload(self): - """ Reload the component if supported """ - pass - - def unload(self): - """ Unload the component and cleanup """ - pass - - - """ Lifecycle Function Hooks """ - def component_initialized(self, *args, **kwargs): - """ Will be called after component `init()` completes """ - pass - - def component_registered(self, *args, **kwargs): - """ Will be called after component added to MudPi """ - pass - - def component_removed(self, *args, **kwargs): - """ Will be called before component is removed from MudPi """ - pass - - - """ INTERNAL METHODS: DO NOT OVERRIDE! - These methods and properties are used internally. - """ - def store_state(self): - """ Stores the current state into the MudPi state managers """ - if self.mudpi is None: - raise MudPiError("MudPi Core instance was not provided!") - - if self.id is None: - raise MudPiError(f"A unique id was not set on component {self.name}!") - - additional_data = {} - - # Used for fontend - if self.metadata: - additional_data.update(self.metadata) - - if self.name: - additional_data.update({'name': self.name}) - - if self.classifier: - additional_data.update({'classifier': self.classifier}) - - data = self.mudpi.states.set(self.id, self.state, additional_data) - - def __repr__(self): - """ Returns the instance representation for debugging. """ - return f'' - -""" Helpers """ -def _is_component(cls): - """ Check if a class is a MudPi component. - Accepts class or instance of class - """ - if not inspect.isclass(cls): - if hasattr(cls, '__class__'): - cls = cls.__class__ - else: - return False - return issubclass(cls, Component) \ No newline at end of file diff --git a/build/lib/mudpi/extensions/action/__init__.py b/build/lib/mudpi/extensions/action/__init__.py deleted file mode 100644 index 4664c50..0000000 --- a/build/lib/mudpi/extensions/action/__init__.py +++ /dev/null @@ -1,118 +0,0 @@ -""" - Actions Extension - Enables MudPi to perfrom operations in response to a trigger. - Components expose their own actions however you can also make - actions manually through configs for more custom interactions. -""" -import json -import subprocess -from mudpi.extensions import Component, BaseExtension - - -NAMESPACE = 'action' -UPDATE_INTERVAL = 30 - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = UPDATE_INTERVAL - - def init(self, config): - self.config = config[self.namespace] - - for entry in self.config: - action = Action(self.mudpi, entry) - self.mudpi.actions.register(action.id, action) - return True - - def validate(self, config): - """ Custom Validation for Action configs - - Requires `key` - """ - key = None - for item in config[self.namespace]: - try: - key = item.get("key") - except Exception as error: - key = None - - if key is None: - raise ConfigError("Missing `key` in configs.") - return config - - -""" Action Templates """ -class Action: - """ Actions perfrom operations in response to a trigger. - - Can be called by MudPi typically with a trigger to - emit an event, run a command, or query a service. - """ - - def __init__(self, mudpi, config): - self.mudpi = mudpi - self.config = config - - self.init() - - def init(self): - """ Action will be different depending on type """ - # Event: json object - # Command: command string - if self.type == 'event': - self.topic = self.config.get("topic", "mudpi") - elif self.type == 'command': - self.shell = self.config.get("shell", False) - - return True - - """ Properties """ - @property - def name(self): - """ Return a friendly name for the Action """ - return self.config.get("name", f"Action-{self.id}") - - @property - def id(self): - """ Returns a unique id for the Action """ - return self.config.get("key", None).replace(" ", "_").lower() if self.config.get( - "key") is not None else self.name.replace(" ", "_").lower() - - """ Custom Properties """ - @property - def type(self): - """ Returns the type of action. (Event or Command) """ - return self.config.get("type", "event") - - @property - def action(self): - """ Returns the action to take """ - return self.config.get("action", None) - - """ Methods """ - def trigger(self, value=None): - """ Trigger the action """ - if self.type == 'event': - self._emit_event() - elif self.type == 'command': - self._run_command(value) - return - - - """ Internal Methods """ - def _emit_event(self): - """ Emit an event """ - self.mudpi.events.publish(self.topic, json.dumps(self.action)) - return - - def _run_command(self, value=None): - """ Run the command """ - 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 - - def __call__(self, val=None): - """ Trigger the action when it is called """ - return self.trigger(val) \ No newline at end of file diff --git a/build/lib/mudpi/extensions/bme680/__init__.py b/build/lib/mudpi/extensions/bme680/__init__.py deleted file mode 100644 index 4e1d4a1..0000000 --- a/build/lib/mudpi/extensions/bme680/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" - BME680 Extension - Includes sensor interface for BME680. - Works on i2c over linux boards. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'bme680' - update_interval = 30 - diff --git a/build/lib/mudpi/extensions/bme680/sensor.py b/build/lib/mudpi/extensions/bme680/sensor.py deleted file mode 100644 index ed8ac3c..0000000 --- a/build/lib/mudpi/extensions/bme680/sensor.py +++ /dev/null @@ -1,107 +0,0 @@ -""" - BME680 Sensor Interface - Connects to a BME680 device to get - environment and climate readings. -""" -import board -import adafruit_bme680 - -from busio import I2C -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - def load(self, config): - """ Load BME680 sensor component from configs """ - sensor = BME680Sensor(self.mudpi, config) - if sensor: - self.add_component(sensor) - return True - - def validate(self, config): - """ Validate the bme680 config """ - if not config.get('address'): - # raise ConfigError('Missing `address` in BME680 config.') - config['address'] = 0x77 - else: - addr = config['address'] - - # Convert hex string/int to actual hex - if isinstance(addr, str): - addr = hex(int(addr, 16)) - elif isinstance(addr, int): - addr = hex(addr) - - config['address'] = addr - - return config - - -class BME680Sensor(Sensor): - """ BME680 Sensor - Gets readins for gas, pressure, humidity, - temperature and altitude. - """ - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return 'climate' - - - """ Methods """ - def init(self): - """ Connect to the device """ - self.i2c = I2C(board.SCL, board.SDA) - self._sensor = adafruit_bme680.Adafruit_BME680_I2C( - self.i2c, address=self.config['address'], debug=False - ) - # Change this to match the location's pressure (hPa) at sea level - self._sensor.sea_level_pressure = self.config.get('calibration_pressure', 1013.25) - - return True - - def update(self): - """ Get data from BME680 device""" - 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: - readings = { - 'temperature': temperature, - 'humidity': humidity, - 'pressure': pressure, - 'gas': gas, - 'altitude': altitude - } - self._state = readings - return readings - else: - Logger.log( - LOG_LEVEL["error"], - 'Failed to get reading [BME680]. Try again!' - ) - - return None diff --git a/build/lib/mudpi/extensions/camera/__init__.py b/build/lib/mudpi/extensions/camera/__init__.py deleted file mode 100644 index 8a4b276..0000000 --- a/build/lib/mudpi/extensions/camera/__init__.py +++ /dev/null @@ -1,193 +0,0 @@ -""" - Camera Extension - Capture images or stream from an IP - (rtsp) camera or raspberry pi camera. -""" -import os -import time -import datetime -from mudpi.extensions import Component, BaseExtension - - -NAMESPACE = 'camera' -UPDATE_INTERVAL = 60 - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = UPDATE_INTERVAL - - def init(self, config): - """ Prepare the extension """ - self.config = config[self.namespace] - - self.manager.init(self.config) - - self.manager.register_component_actions('capture', action='capture_image') - self.manager.register_component_actions('record', action='capture_recording') - return True - - -class Camera(Component): - """ Base Camera - Base Camera for all camera interfaces - """ - - # A string of the last image taken - last_image = None - - # Number of images captured - image_count = 0 - - # Duration tracking. Set high to cause capture on first load - _duration_start = -(60 * 60 * 24) - - """ Properties """ - @property - def id(self): - """ Unique id or key """ - return self.config.get('key') - - @property - def name(self): - """ Friendly name of control """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ State is the last image for display """ - return self.last_image - - @property - def filename(self): - """ Return the name you want images and recordings saved as - Image: {filename}-{image-count} - Video: {filename}-{date} - """ - return self.config.get('filename', self.id) + f'_{self.file_suffix}' - - @property - def file_suffix(self): - """ Return file suffix based on `sequential_naming`""" - return f'{self.image_count:05}' if self.sequential_naming else \ - datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") - - - @property - def count_start(self): - """ Return starting count """ - return self.config.get('count_start', 0) - - @property - def max_count(self): - """ Return max number of images before it overwrites""" - return self.config.get('max_count', 500) - - @property - def sequential_naming(self): - """ Return True if files should save with - counter instead of datetime - """ - return self.config.get('sequential_naming', False) - - @property - def path(self): - """ Return path to save files too""" - return os.path.join(self.config.get('path', os.getcwd())) - - @property - def width(self): - """ Return width in px """ - return self.config.get('resolution', {}).get('x',1920) #1280 - - @property - def height(self): - """ Return height in px """ - return self.config.get('resolution', {}).get('y',1080) #720 - - @property - def resolution(self): - """ Return a dict of (width in px, height in px) - Default: 1080p - """ - return {'x': self.width, 'y': self.height} - - @property - def framerate(self): - """ Return frames per seconds for recording """ - return self.config.get('framerate', 15) - - @property - def delay(self): - """ Return a dict with delay photos should be taken """ - _delay = self.config.get('delay', {"hours":0, "minutes":10, "seconds":0}) - return datetime.timedelta( - hours=_delay.get('hours', 0), minutes=_delay.get('minutes', 0), - seconds=_delay.get('seconds', 0)) - - @property - def next_interval(self): - """ Return next time after delay """ - return (datetime.datetime.now() + self.delay).replace(microsecond=0) - - @property - def topic(self): - """ Return topic to listen for commands on """ - return self.config.get('topic', f'{NAMESPACE}/{self.id}') - - @property - def duration(self): - """ Return how long the current state has been applied in seconds """ - self._current_duration = time.perf_counter() - self._duration_start - return round(self._current_duration, 4) - - @property - def record_duration(self): - """ Return number of seconds for recording """ - return self.config.get('record_duration', 5) - - - """ Methods """ - def fire(self, data={}): - """ Fire a control event """ - event_data = { - 'event': 'CameraUpdated', - 'component_id': self.id, - 'name': self.name, - 'updated_at': str(datetime.datetime.now().replace(microsecond=0)), - 'state': self.state, - 'last_image': self.last_image, - } - event_data.update(data) - self.mudpi.events.publish(NAMESPACE, event_data) - - def reset_duration(self): - """ Reset the duration of the current state """ - self._duration_start = time.perf_counter() - return self._duration_start - - def check_path(self, path_addon=None): - """ Checks the set path and makes sure directories exist """ - _path = os.path.join(self.path) if not path_addon else os.path.join(self.path, path_addon) - if not os.path.exists(_path): - # Attempt to create missing directory - os.makedirs(self.path, exist_ok=True) - - def increment_count(self): - """ Update the image counter """ - self.image_count +=1 - if self.image_count > self.max_count: - self.image_count = 0 # overflow - - - """ Actions """ - def capture_image(self, data={}): - """ Capture a single image from the camera - it should use the file name and increment - counter for sequenetial images """ - # call self.increment_count() after each image saved - pass - - def capture_recording(self, data={}): - """ Record a video from the camera """ - pass - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/char_display/__init__.py b/build/lib/mudpi/extensions/char_display/__init__.py deleted file mode 100644 index b540453..0000000 --- a/build/lib/mudpi/extensions/char_display/__init__.py +++ /dev/null @@ -1,222 +0,0 @@ -""" - LCD Character Displays Extension - Displays are very useful to output - messages from the system. Character - displays supported 16x2 and 20x4. -""" -import re -import time -import json -from mudpi.utils import decode_event_data -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.extensions import Component, BaseExtension - - -NAMESPACE = 'char_display' - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = 0.5 - - def init(self, config): - self.config = config[self.namespace] - - self.manager.init(self.config) - - self.manager.register_component_actions('show', action='show') - self.manager.register_component_actions('clear', action='clear') - self.manager.register_component_actions('clear_queue', action='clear_queue') - self.manager.register_component_actions('next_message', action='next_message') - self.manager.register_component_actions('turn_on_backlight', action='turn_on_backlight') - self.manager.register_component_actions('turn_off_backlight', action='turn_off_backlight') - return True - - -class CharDisplay(Component): - """ Base CharDisplay - Base Character Display Class - """ - - # Current message being displayed - current_message = '' - - # Check current_message to display once / prevent flickers - cached_message = { - 'message': '', - 'duration': 3 - } - - # Bool if message should be rotated - message_expired = True - - # Queue of messages to display - queue = [] #queue.Queue() - - # Duration tracking - _duration_start = time.perf_counter() - - @property - def id(self): - """ Unique id or key """ - return self.config.get('key') - - @property - def name(self): - """ Friendly name of control """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return state of the display """ - return self.current_message - - @property - def default_duration(self): - """ Default message display duration """ - return int(self.config.get('default_duration', 5)) - - @property - def max_duration(self): - """ Message max display duration to prevent display lock """ - return int(self.config.get('max_duration', 60)) - - @property - def message_limit(self): - """ Max number of messages before overwriting """ - return int(self.config.get('message_limit', 20)) - - @property - def topic(self): - """ Max number of messages before overwriting """ - return str(self.config.get('topic', f'{NAMESPACE}/{self.id}')) - - @property - def duration(self): - """ Return how long the current state has been applied in seconds """ - self._current_duration = time.perf_counter() - self._duration_start - return round(self._current_duration, 4) - - - """ Actions """ - def show(self, data=None): - """ Show a message on the screen """ - pass - - def clear(self, data=None): - """ Clear the display screen """ - pass - - def turn_on_backlight(self, data=None): - """ Turn the backlight on """ - pass - - def turn_off_backlight(self, data=None): - """ Turn the backlight on """ - pass - - def clear_queue(self, data=None): - """ Clear the message queue """ - self.queue = [] - Logger.log(LOG_LEVEL["debug"], - f'Cleared the Message Queue for {self.id}') - - def next_message(self, data={}): - """ Advances to the next message """ - self.message_expired = True - - - """ Methods """ - def init(self): - """ Setup the display to listen for events """ - self.mudpi.events.subscribe(self.topic, self.handle_event) - - def update(self): - """ Check if messages need to display from queue """ - if self.mudpi.is_prepared: - if self.duration > self.cached_message['duration'] + 1: - self.message_expired = True - - if self.message_expired: - # Get first time message after clear - self.cached_message = self.get_next_message() - - if self.current_message != self.cached_message.get('message', ''): - self.clear() - time.sleep(0.004) # time to finish clear - self.show(self.cached_message) - self.reset_duration() - # store message to only display once and prevent flickers - self.current_message = self.cached_message['message'] - else: - # System not ready - self.reset_duration() - - def add_message(self, data={}): - """ Add message to display queue """ - message = data.get('message', '') - duration = data.get('duration', self.default_duration) - if duration > self.max_duration: - duration = self.max_duration - - # Replace any codes such as [temperature] with a value - # found in the state manager. TODO: add templates instead - short_codes = re.findall(r'\[(.*?) *\]', message) - - for code in short_codes: - data = self.mudpi.states.get(code) - - if data is None: - data = '' - - else: - try: - data = json.loads(state.state) - except Exception: - data = '' - message = message.replace('[' + code + ']', str(data)) - - new_message = { - "message": message.replace("\\n", "\n"), - "duration": duration - } - - if len(self.queue) >= self.message_limit: - self.queue.pop(0) - - self.queue.append(new_message) - - _event = {'event': 'MessageQueued', 'data': new_message} - self.mudpi.events.publish(NAMESPACE, _event) - return - - def get_next_message(self): - """ Get the next message from queue """ - if len(self.queue) > 0: - self.message_expired = False - self.reset_duration() - return self.queue.pop(0) - return {'message': '', 'duration': self.default_duration} - - def reset_duration(self): - """ Reset the duration of the current state """ - self._duration_start = time.perf_counter() - return self._duration_start - - def handle_event(self, event): - """ Handle events from event system """ - data = event['data'] - if data is not None: - _event = decode_event_data(data) - try: - if _event['event'] == 'Message': - if _event.get('data', None): - _duration = _event['data'].get('duration', self.default_duration) - _message = _event['data'].get('message', '') - self.add_message(_message, int(_duration)) - elif _event['event'] == 'Clear': - self.clear() - elif _event['event'] == 'ClearQueue': - self.clear_queue() - except Exception: - Logger.log(LOG_LEVEL["error"], - f'Error Handling Event for {self.id}') diff --git a/build/lib/mudpi/extensions/control/__init__.py b/build/lib/mudpi/extensions/control/__init__.py deleted file mode 100644 index 1d8aa5d..0000000 --- a/build/lib/mudpi/extensions/control/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - Controls Extension - Controls are components like buttons, switches, - potentiometers, etc. They are utilized to get - user input into the system. -""" -import datetime -from mudpi.extensions import Component, BaseExtension - - -NAMESPACE = 'control' - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = 0.5 - - def init(self, config): - self.config = config[self.namespace] - - self.manager.init(self.config) - return True - - - -class Control(Component): - """ Base Control - Base class for all controls. - """ - - @property - def id(self): - """ Unique id or key """ - return self.config.get('key') - - @property - def name(self): - """ Friendly name of control """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def pin(self): - """ The GPIO pin """ - return self.config.get('pin') - - @property - def resistor(self): - """ Set internal resistor to pull UP or DOWN """ - return self.config.get('resistor') - - @property - def debounce(self): - """ Used to smooth out ripples and false fires """ - return self.config.get('debounce') - - @property - def type(self): - """ Button, Switch, Potentiometer """ - return self.config.get('type', 'button').lower() - - @property - def edge_detection(self): - """ Return if edge detection is used """ - _edge_detection = config.get('edge_detection') - if _edge_detection is not None: - if _edge_detection == "falling" or _edge_detection == "fell": - _edge_detection = "fell" - elif _edge_detection == "rising" or _edge_detection == "rose": - _edge_detection = "rose" - elif _edge_detection == "both": - _edge_detection = "both" - return _edge_detection - - @property - def invert_state(self): - """ Set to True to make OFF state fire events instead of ON state """ - return self.config.get('invert_state', False) - - - """ Methods """ - def fire(self): - """ Fire a control event """ - event_data = { - 'event': 'ControlUpdated', - 'component_id': self.id, - 'type': self.type, - 'name': self.name, - 'updated_at': str(datetime.datetime.now().replace(microsecond=0)), - 'state': self.state, - 'invert_state': self.invert_state - } - self.mudpi.events.publish(NAMESPACE, event_data) \ No newline at end of file diff --git a/build/lib/mudpi/extensions/control/trigger.py b/build/lib/mudpi/extensions/control/trigger.py deleted file mode 100644 index d0e7441..0000000 --- a/build/lib/mudpi/extensions/control/trigger.py +++ /dev/null @@ -1,91 +0,0 @@ -""" - Control Trigger Interface - Monitors control state changes and - checks new state against any - thresholds if provided. -""" -from mudpi.utils import decode_event_data -from mudpi.exceptions import ConfigError -from mudpi.extensions import BaseInterface -from mudpi.extensions.trigger import Trigger -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class Interface(BaseInterface): - - def load(self, config): - """ Load control Trigger component from configs """ - trigger = ControlTrigger(self.mudpi, config) - if trigger: - self.add_component(trigger) - return True - - def validate(self, config): - """ Validate the trigger config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('source'): - raise ConfigError('Missing `source` key in Sensor Trigger config.') - - return config - - -class ControlTrigger(Trigger): - """ A trigger that listens to states - and checks for new state that - matches any thresholds. - """ - - # Used for onetime subscribe - _listening = False - - - def init(self): - """ Listen to the state for changes """ - super().init() - if self.mudpi.is_prepared: - if not self._listening: - # TODO: Eventually get a handler returned to unsub just this listener - self.mudpi.events.subscribe('control', self.handle_event) - self._listening = True - return True - - """ Methods """ - def handle_event(self, event): - """ Handle the event data from the event system """ - _event_data = decode_event_data(event) - if _event_data.get('event'): - try: - if _event_data['event'] == 'ControlUpdated': - if _event_data['component_id'] == self.source: - _value = self._parse_data(_event_data["state"]) - if self.evaluate_thresholds(_value): - self.active = True - if self._previous_state != self.active: - # Trigger is reset, Fire - self.trigger(_event_data) - else: - # Trigger not reset check if its multi fire - if self.frequency == 'many': - self.trigger(_event_data) - else: - self.active = False - except Exception as error: - Logger.log(LOG_LEVEL["error"], - f'Error evaluating thresholds for trigger {self.id}') - Logger.log(LOG_LEVEL["debug"], error) - self._previous_state = self.active - - def unload(self): - # Unsubscribe once bus supports single handler unsubscribes - return - - def _parse_data(self, data): - """ Get nested data if set otherwise return the data """ - if isinstance(data, dict): - print('dict con') - return data if not self.nested_source else data.get(self.nested_source, None) - return data - diff --git a/build/lib/mudpi/extensions/cron/__init__.py b/build/lib/mudpi/extensions/cron/__init__.py deleted file mode 100644 index cb2821a..0000000 --- a/build/lib/mudpi/extensions/cron/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" - Cron Extension - Cron schedule support for triggers - to allow scheduling. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'cron' - update_interval = 1 - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/cron/trigger.py b/build/lib/mudpi/extensions/cron/trigger.py deleted file mode 100644 index 8cd0f8e..0000000 --- a/build/lib/mudpi/extensions/cron/trigger.py +++ /dev/null @@ -1,75 +0,0 @@ -""" - Cron Trigger Interface - Cron schedule support for triggers - to allow scheduling. -""" -import time -import pycron -from mudpi.exceptions import ConfigError -from mudpi.extensions import BaseInterface -from mudpi.extensions.trigger import Trigger -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class Interface(BaseInterface): - - # Override the update time - update_interval = 60 - - def load(self, config): - """ Load cron trigger component from configs """ - trigger = CronTrigger(self.mudpi, config) - if trigger: - self.add_component(trigger) - return True - - def validate(self, config): - """ Validate the trigger config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('schedule'): - Logger.log( - LOG_LEVEL["debug"], - 'Trigger: No `schedule`, defaulting to every 5mins' - ) - # raise ConfigError('Missing `schedule` in Trigger config.') - - return config - - -class CronTrigger(Trigger): - """ A trigger that resoponds to time - changes based on cron schedule string - """ - - """ Properties """ - @property - def schedule(self): - """ Cron schedule string to check time against """ - return self.config.get('schedule', '*/5 * * * *') - - - """ Methods """ - def init(self): - """ Pass call to parent """ - super().init() - - def check(self): - """ Check trigger schedule thresholds """ - if self.mudpi.is_running: - try: - if pycron.is_now(self.schedule): - if not self.active: - self.trigger() - self.active = True - else: - self.active = False - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - "Error evaluating time trigger schedule." - ) - return - diff --git a/build/lib/mudpi/extensions/dht/__init__.py b/build/lib/mudpi/extensions/dht/__init__.py deleted file mode 100644 index de64716..0000000 --- a/build/lib/mudpi/extensions/dht/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" - DHT Extension - Includes sensor interface for DHT. - Works with DHT11, DHT22, DHT2203 -""" -from mudpi.extensions import BaseExtension - - -NAMESPACE = 'dht' -UPDATE_INTERVAL = 30 - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = UPDATE_INTERVAL - diff --git a/build/lib/mudpi/extensions/dht/sensor.py b/build/lib/mudpi/extensions/dht/sensor.py deleted file mode 100644 index e33b53b..0000000 --- a/build/lib/mudpi/extensions/dht/sensor.py +++ /dev/null @@ -1,133 +0,0 @@ -""" - DHT Sensor Interface - Connects to a DHT device to get - humidity and temperature readings. -""" -import re -import board -import adafruit_dht -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - def load(self, config): - """ Load DHT sensor component from configs """ - sensor = DHTSensor(self.mudpi, config) - if sensor: - self.add_component(sensor) - return True - - def validate(self, config): - """ Validate the dht config """ - if not config.get('pin'): - raise ConfigError('Missing `pin` in DHT config.') - - if not re.match(r'D\d+$', config['pin']) and not re.match(r'A\d+$', config['pin']): - raise ConfigError( - "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" - ) - - valid_models = ['11', '22', '2302'] - if config.get('model') not in valid_models: - config['model'] = '11' - Logger.log( - LOG_LEVEL["warning"], - 'Sensor Model Error: Defaulting to DHT11' - ) - - return config - - -class DHTSensor(Sensor): - """ DHT Sensor - Returns a random number - """ - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return 'climate' - - - """ Methods """ - def init(self): - """ Connect to the device """ - self.pin_obj = getattr(board, self.config['pin']) - self.type = self.config['model'] - - sensor_types = { - '11': adafruit_dht.DHT11, - '22': adafruit_dht.DHT22, - '2302': adafruit_dht.DHT22 - } # AM2302 = DHT22 - - if self.type in sensor_types: - self._dht_device = sensor_types[self.type] - - try: - self._sensor = self._dht_device(self.pin_obj) - Logger.log( - LOG_LEVEL["debug"], - 'Sensor Initializing: DHT' - ) - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - 'Sensor Initialize Error: DHT Failed to Init' - ) - return False - return True - - def update(self): - """ Get data from DHT device""" - humidity = None - temperature_c = None - - try: - # Calling temperature or humidity triggers measure() - temperature_c = self._sensor.temperature - humidity = self._sensor.humidity - except RuntimeError as error: - # Errors happen fairly often, DHT's are hard to read - Logger.log(LOG_LEVEL["error"], error) - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - 'DHT Device Encountered an Error.' - ) - self._sensor.exit() - - if humidity is not None and temperature_c is not None: - readings = { - 'temperature': round(temperature_c * 1.8 + 32, 2), - 'humidity': round(humidity, 2) - } - self._state = readings - else: - Logger.log( - LOG_LEVEL["error"], - 'DHT Reading was Invalid. Trying again next cycle.' - ) - return None diff --git a/build/lib/mudpi/extensions/example/__init__.py b/build/lib/mudpi/extensions/example/__init__.py deleted file mode 100644 index aafa483..0000000 --- a/build/lib/mudpi/extensions/example/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" - Example Extension - Includes some example configs for testing - and has interfaces for core components. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'example' - update_interval = 30 - diff --git a/build/lib/mudpi/extensions/example/char_display.py b/build/lib/mudpi/extensions/example/char_display.py deleted file mode 100644 index 1f2660c..0000000 --- a/build/lib/mudpi/extensions/example/char_display.py +++ /dev/null @@ -1,65 +0,0 @@ -""" - Example Character Display - Stores Message in Memory. -""" - -from mudpi.extensions import BaseInterface -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.extensions.char_display import CharDisplay - - -class Interface(BaseInterface): - - def load(self, config): - """ Load example display component from configs """ - display = ExampleDisplay(self.mudpi, config) - - # Check for test messages to fill the queue with - if config.get('messages'): - _count = 0 - while _count < display.message_limit: - for msg in config['messages']: - display.add_message({'message': msg}) - _count +=1 - - if display: - self.add_component(display) - return True - - - def validate(self, config): - """ Validate the display configs """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in example display config.') - - return config - - -class ExampleDisplay(CharDisplay): - """ Example Character Display - Test display that keeps messages in memory. - """ - - """ Properties """ - @property - def default_duration(self): - """ Default message display duration """ - return int(self.config.get('default_duration', 10)) - - - """ Actions """ - def clear(self, data=None): - """ Clear the display screen """ - self.current_message = '' - - def show(self, data={}): - """ Show a message on the display """ - if not isinstance(data, dict): - data = {'message': data} - - self.current_message = data.get('message', '') - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/example/control.py b/build/lib/mudpi/extensions/example/control.py deleted file mode 100644 index 3ff59f3..0000000 --- a/build/lib/mudpi/extensions/example/control.py +++ /dev/null @@ -1,90 +0,0 @@ -""" - Example Control Interface - Example control that randomly fires - events for demonstration. -""" -import random -from mudpi.extensions import BaseInterface -from mudpi.extensions.control import Control -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - # Examples don't need to be ultra fast - update_interval = 3 - - def load(self, config): - """ Load example control component from configs """ - control = ExampleControl(self.mudpi, config) - if control: - self.add_component(control) - return True - - def validate(self, config): - """ Validate the control config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if conf.get('key') is None: - raise ConfigError('Missing `key` in example control.') - - return config - - - -class ExampleControl(Control): - """ Example Control - Randomly fires active for demonstration of controls - """ - - # Default inital state - _state = False - - # One time firing - _fired = False - - """ Properties """ - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def update_chance(self): - """ Return the chance the trigger will fire (1-100) - Default: 25% """ - _chance = self.config.get('update_chance', 25) - if _chance > 100 or _chance < 1: - _chance = 25 - return _chance - - - """ Methods """ - def update(self): - """ Check if control should flip state randomly """ - _state = self._state - if random.randint(1, 100) <= self.update_chance: - _state = not _state - self._state = _state - self.handle_state() - - def handle_state(self): - """ Control logic depending on type of control """ - if self.type == 'button': - if self._state: - if not self.invert_state: - self.fire() - else: - if self.invert_state: - self.fire() - elif self.type == 'switch': - # Switches use debounce ensuring we only fire once - if self._state and not self._fired: - # State changed since we are using edge detect - self.fire() - self._fired = True - else: - self._fired = False - diff --git a/build/lib/mudpi/extensions/example/sensor.py b/build/lib/mudpi/extensions/example/sensor.py deleted file mode 100644 index 471b00a..0000000 --- a/build/lib/mudpi/extensions/example/sensor.py +++ /dev/null @@ -1,52 +0,0 @@ -""" - Example Sensor Interface - Returns a random number between one - and ten with each update. -""" -import random -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor - - -class Interface(BaseInterface): - - def load(self, config): - """ Load example sensor component from configs """ - sensor = ExampleSensor(self.mudpi, config) - self.add_component(sensor) - return True - - -class ExampleSensor(Sensor): - """ Example Sensor - Returns a random number - """ - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return self.config.get('classifier', "general") - - - """ Methods """ - def update(self): - """ Get Random data """ - self._state = random.randint(1, self.config.get('data', 10)) - return True - diff --git a/build/lib/mudpi/extensions/example/toggle.py b/build/lib/mudpi/extensions/example/toggle.py deleted file mode 100644 index 1011d7c..0000000 --- a/build/lib/mudpi/extensions/example/toggle.py +++ /dev/null @@ -1,64 +0,0 @@ -""" - Example Toggle Interface - Example toggle for testing. State - is stored in memory. -""" -from mudpi.extensions import BaseInterface -from mudpi.extensions.toggle import Toggle -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - def load(self, config): - """ Load example toggle component from configs """ - toggle = ExampleToggle(self.mudpi, config) - if toggle: - self.add_component(toggle) - return True - - def validate(self, config): - """ Validate the example config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if conf.get('key') is None: - raise ConfigError('Missing `key` in example toggle config.') - - return config - - -class ExampleToggle(Toggle): - """ Example Toggle - Turns a boolean off and on in memory - """ - - """ Methods """ - def restore_state(self, state={}): - """ This is called on start to - restore previous state """ - self._state = True if state.get('state', False) else False - return - - - """ Actions """ - def toggle(self, data={}): - # Toggle the state - if self.mudpi.is_prepared: - self.active = not self.active - self.store_state() - - def turn_on(self, data={}): - # Turn on if its not on - if self.mudpi.is_prepared: - if not self.active: - self.active = True - self.store_state() - - def turn_off(self, data={}): - # Turn off if its not off - if self.mudpi.is_prepared: - if self.active: - self.active = False - self.store_state() \ No newline at end of file diff --git a/build/lib/mudpi/extensions/gpio/__init__.py b/build/lib/mudpi/extensions/gpio/__init__.py deleted file mode 100644 index 02729ff..0000000 --- a/build/lib/mudpi/extensions/gpio/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" - GPIO Extension - Includes interfaces for linux board - GPIO. Supports many linux based boards. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'gpio' - update_interval = 30 - diff --git a/build/lib/mudpi/extensions/gpio/control.py b/build/lib/mudpi/extensions/gpio/control.py deleted file mode 100644 index 6f880a4..0000000 --- a/build/lib/mudpi/extensions/gpio/control.py +++ /dev/null @@ -1,122 +0,0 @@ -""" - GPIO Control Interface - Connects to a linux board GPIO to - take analog or digital readings. -""" -import board -import digitalio -from adafruit_debouncer import Debouncer -from mudpi.extensions import BaseInterface -from mudpi.extensions.control import Control -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - def load(self, config): - """ Load GPIO control component from configs """ - control = GPIOControl(self.mudpi, config) - if control: - self.add_component(control) - return True - - def validate(self, config): - """ Validate the control config """ - if config.get('key') is None: - raise ConfigError('Missing `key` in example control.') - - if config.get('pin') is None: - raise ConfigError('Missing `pin` in GPIO config.') - - if not re.match(r'D\d+$', config['pin']) and not re.match(r'A\d+$', config['pin']): - raise ConfigError( - "Cannot detect pin type (Digital or analog), " - "should be D## or A## for digital or analog. " - "Please refer to " - "https://github.com/adafruit/Adafruit_Blinka/tree/master/src/adafruit_blinka/board" - ) - - -class GPIOControl(Control): - """ GPIO Control - Get GPIO input via button, switch, etc. - """ - - """ Properties """ - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - - """ Methods """ - def init(self): - """ Connect to the device """ - self.pin_obj = getattr(board, self.pin) - self.gpio = digitalio - self.previous_state = 0 - - if re.match(r'D\d+$', self.pin): - self.is_digital = True - elif re.match(r'A\d+$', self.pin): - self.is_digital = False - - if self.resistor is not None: - if self.resistor == "up" or self.resistor == digitalio.Pull.UP: - self.resistor = digitalio.Pull.UP - elif self.resistor == "down" or self.resistor == digitalio.Pull.DOWN: - self.resistor = digitalio.Pull.DOWN - else: - # Unknown resistor pull, defaulting to None - self.config['resistor'] = None - - self._control_pin = self.gpio.DigitalInOut(self.pin_obj) - self._control_pin.switch_to_input(pull=self.resistor) - - # Switches use debounce for better detection - # TODO: get rid of this to allow long press, release, and press detection - if self.type == 'switch': - self.config['edge_detection'] = 'both' - - if self.edge_detection is not None: - self._control = Debouncer(self._control_pin) - if self.debounce is not None: - self._control.interval = self.debounce - - return True - - def update(self): - """ Get data from GPIO connection""" - data = None - if self.edge_detection is not None: - self._control.update() - if self.edge_detection == "both": - if self._control.fell or self._control.rose: - # Pressed or Released - data = 1 - else: - data = 0 - else: # "fell" or "rose" - data = 1 if getattr(self._control, self.edge_detection) else 0 - else: - data = 1 if self._control_pin.value else 0 - self.previous_state = self._state - self._state = data - self.handle_state() - return data - - def handle_state(self): - """ Control logic depending on type of control """ - if self.type == 'button': - if self._state: - if not self.invert_state: - self.fire() - else: - if self.invert_state: - self.fire() - elif self.type == 'switch': - # Switches use debounce ensuring we only fire once - if self._state: - # State changed since we are using edge detect - self.fire() - diff --git a/build/lib/mudpi/extensions/gpio/sensor.py b/build/lib/mudpi/extensions/gpio/sensor.py deleted file mode 100644 index d29ec6d..0000000 --- a/build/lib/mudpi/extensions/gpio/sensor.py +++ /dev/null @@ -1,86 +0,0 @@ -""" - GPIO Sensor Interface - Connects to a linux board GPIO to - take analog or digital readings. -""" -import board -import digitalio -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - def load(self, config): - """ Load GPIO sensor component from configs """ - sensor = GPIOSensor(self.mudpi, config) - if sensor: - self.add_component(sensor) - return True - - def validate(self, config): - """ Validate the dht config """ - if not config.get('pin'): - raise ConfigError('Missing `pin` in GPIO config.') - - if not re.match(r'D\d+$', config['pin']) and not re.match(r'A\d+$', config['pin']): - raise ConfigError( - "Cannot detect pin type (Digital or analog), " - "should be D## or A## for digital or analog. " - "Please refer to " - "https://github.com/adafruit/Adafruit_Blinka/tree/master/src/adafruit_blinka/board" - ) - - return config - - -class GPIOSensor(Sensor): - """ GPIO Sensor - Returns a reading from gpio pin - """ - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return 'general' - - - """ Methods """ - def init(self): - """ Connect to the device """ - self.pin_obj = getattr(board, self.config['pin']) - - if re.match(r'D\d+$', pin): - self.is_digital = True - elif re.match(r'A\d+$', pin): - self.is_digital = False - - self.gpio = digitalio - - return True - - def update(self): - """ Get data from GPIO connection""" - if self.is_digital: - data = self.gpio.DigitalInOut(self.pin_obj).value - else: - data = self.gpio.AnalogIn(self.pin_obj).value - self._state = data - return data diff --git a/build/lib/mudpi/extensions/gpio/toggle.py b/build/lib/mudpi/extensions/gpio/toggle.py deleted file mode 100644 index d84ab7d..0000000 --- a/build/lib/mudpi/extensions/gpio/toggle.py +++ /dev/null @@ -1,118 +0,0 @@ -""" - GPIO Toggle Interface - Connects to a linux board GPIO to - toggle output on and off. Useful for - turning things on like lights or pumps. -""" -import board -import digitalio -from mudpi.extensions import BaseInterface -from mudpi.extensions.toggle import Toggle -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - def load(self, config): - """ Load GPIO toggle component from configs """ - toggle = GPIOToggle(self.mudpi, config) - if toggle: - self.add_component(toggle) - return True - - def validate(self, config): - """ Validate the dht config """ - if config.get('key') is None: - raise ConfigError('Missing `key` in GPIO toggle config.') - - if config.get('pin') is None: - raise ConfigError('Missing `pin` in GPIO toggle config.') - - if not re.match(r'D\d+$', config['pin']) and not re.match(r'A\d+$', config['pin']): - raise ConfigError( - "Cannot detect pin type (Digital or analog), " - "should be D## or A## for digital or analog. " - "Please refer to " - "https://github.com/adafruit/Adafruit_Blinka/tree/master/src/adafruit_blinka/board" - ) - - return config - - -class GPIOToggle(Toggle): - """ GPIO Toggle - Turns a GPIO pin on or off - """ - - """ Properties """ - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self.gpio_pin.value - - @property - def pin(self): - """ The GPIO pin """ - return self.config.get('pin') - - - """ Methods """ - def init(self): - """ Connect to the device """ - self.pin_obj = getattr(board, self.pin) - - if re.match(r'D\d+$', self.pin): - self.is_digital = True - elif re.match(r'A\d+$', self.pin): - self.is_digital = False - - if self.invert_state: - self.pin_state_on = False - self.pin_state_off = True - else: - self.pin_state_on = True - self.pin_state_off = False - - self.gpio = digitalio - self.gpio_pin = digitalio.DigitalInOut(self.pin_obj) - self.gpio_pin.switch_to_output() - - self.gpio_pin.value = self.pin_state_off - # Active is used to keep track of durations - self.active = False - time.sleep(0.1) - - return True - - def restore_state(self, state={}): - """ This is called on start to - restore previous state """ - self.gpio_pin.value = self.pin_state_on if state.get('state', False) else self.pin_state_off - return - - - """ Actions """ - def toggle(self, data={}): - # Toggle the GPIO state - if self.mudpi.is_prepared: - # Do inverted check and change value before setting active - # to avoid false state being provided in the event fired. - self.gpio_pin.value = self.pin_state_on if not self.active else self.pin_state_off - self.active = not self.active - self.store_state() - - def turn_on(self, data={}): - # Turn on GPIO if its not on - if self.mudpi.is_prepared: - if not self.active: - self.gpio_pin.value = self.pin_state_on - self.active = True - self.store_state() - - def turn_off(self, data={}): - # Turn off GPIO if its not off - if self.mudpi.is_prepared: - if self.active: - self.gpio_pin.value = self.pin_state_off - self.active = False - self.store_state() \ No newline at end of file diff --git a/build/lib/mudpi/extensions/group/__init__.py b/build/lib/mudpi/extensions/group/__init__.py deleted file mode 100644 index 6ad8244..0000000 --- a/build/lib/mudpi/extensions/group/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" - Group Extension - Allows grouping of components. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'group' - update_interval = 0.2 - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/group/trigger.py b/build/lib/mudpi/extensions/group/trigger.py deleted file mode 100644 index a51d290..0000000 --- a/build/lib/mudpi/extensions/group/trigger.py +++ /dev/null @@ -1,83 +0,0 @@ -""" - Group Trigger Interface - Allows triggers to be grouped - together for complex conditions. -""" -from mudpi.exceptions import ConfigError -from mudpi.extensions import BaseInterface -from mudpi.extensions.trigger import Trigger -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class Interface(BaseInterface): - - def load(self, config): - """ Load group trigger component from configs """ - trigger = GroupTrigger(self.mudpi, config) - if trigger: - self.add_component(trigger) - return True - - def validate(self, config): - """ Validate the trigger config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('triggers'): - raise ConfigError('Missing `triggers` keys in Trigger Group') - - return config - - -class GroupTrigger(Trigger): - """ A Group to allow complex combintations - between multiple trigger types. - """ - - # List of triggers to monitor - _triggers = [] - - """ Properties """ - @property - def triggers(self): - """ Keys of triggers to group """ - return self.config.get('triggers', []) - - @property - def trigger_states(self): - """ Keys of triggers to group """ - return [trigger.active for trigger in self._triggers] - - - """ Methods """ - def init(self): - """ Load in the triggers for the group """ - # Doesnt call super().init() because that is for non-groups - self.cache = self.mudpi.cache.get('trigger', {}) - self.cache.setdefault('groups', {})[self.id] = self - - for _trigger in self.triggers: - _trig = self.cache.get('triggers', {}).get(_trigger) - if _trig: - self.add_trigger(_trig) - return True - - def add_trigger(self, trigger): - """ Add a trigger to monitor """ - self._triggers.append(trigger) - - def check(self): - """ Check if trigger should fire """ - if all(self.trigger_states): - self.active = True - if self._previous_state != self.active: - # Trigger is reset, Fire - self.trigger() - else: - # Trigger not reset check if its multi fire - if self.frequency == 'many': - self.trigger() - else: - self.active = False - self._previous_state = self.active diff --git a/build/lib/mudpi/extensions/i2c/__init__.py b/build/lib/mudpi/extensions/i2c/__init__.py deleted file mode 100644 index 06bda48..0000000 --- a/build/lib/mudpi/extensions/i2c/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -""" - I2C Extension - Supports I2C protocol and - provides interfaces for components. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'i2c' - update_interval = 0.5 diff --git a/build/lib/mudpi/extensions/i2c/char_display.py b/build/lib/mudpi/extensions/i2c/char_display.py deleted file mode 100644 index 704ba4a..0000000 --- a/build/lib/mudpi/extensions/i2c/char_display.py +++ /dev/null @@ -1,135 +0,0 @@ -""" - Character Display Interface - Connects to a LCD character - display through a linux I2C. -""" -import time -import json -import redis -import board -import busio -import datetime -from mudpi.extensions import BaseInterface -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.extensions.char_display import CharDisplay -import adafruit_character_lcd.character_lcd_rgb_i2c as character_rgb_lcd -import adafruit_character_lcd.character_lcd_i2c as character_lcd - - -class Interface(BaseInterface): - - def load(self, config): - """ Load display component from configs """ - display = I2CCharDisplay(self.mudpi, config) - if display: - self.add_component(display) - return True - - - def validate(self, config): - """ Validate the display configs """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in i2c display config.') - - if not conf.get('address'): - # raise ConfigError('Missing `address` in i2c char_lcd config.') - conf['address'] = 0x27 - else: - addr = conf['address'] - - # Convert hex string/int to actual hex - if isinstance(addr, str): - addr = hex(int(addr, 39)) - elif isinstance(addr, int): - addr = hex(addr) - - conf['address'] = addr - - if not isinstance conf.get('columns', 16): - raise ConfigError('Missing `columns` must be an int.') - - if not isinstance conf.get('rows', 2): - raise ConfigError('Missing `rows` must be an int.') - - return config - - -class I2CCharDisplay(CharDisplay): - """ I2C Character Display - Displays messages through i2c lcd. - """ - - @property - def address(self): - """ Unique id or key """ - return self.config.get('address', 0x27) - - @property - def model(self): - """ Return the model (rgb, i2c, pcf) """ - if self.config.get('model', 'i2c') not in ('rgb', 'i2c', 'pcf'): - self.config['model'] = 'i2c' - return self.config.get('model', 'i2c').lower() - - - """ Actions """ - def clear(self, data=None): - """ Clear the display screen """ - self.lcd.clear() - Logger.log(LOG_LEVEL["debug"], 'Cleared the LCD Screen') - - def show(self, data={}): - """ Show a message on the display """ - if not isinstance(data, dict): - data = {'message': data} - - self.lcd.message = data.get('message', '') - - def turn_on_backlight(self): - """ Turn the backlight on """ - self.lcd.backlight = True - - def turn_off_backlight(self): - """ Turn the backlight on """ - self.lcd.backlight = False - - - """ Methods """ - def init(self): - """ Connect to the display over I2C """ - super().init() - - # Prepare the display i2c connection - self.i2c = busio.I2C(board.SCL, board.SDA) - - if self.model == 'rgb': - self.lcd = character_lcd.Character_LCD_RGB_I2C( - self.i2c, - self.columns, - self.rows, - self.address - ) - - elif self.model == '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 - ) - - self.turn_on_backlight() - self.clear() - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/mqtt/__init__.py b/build/lib/mudpi/extensions/mqtt/__init__.py deleted file mode 100644 index fb6af8d..0000000 --- a/build/lib/mudpi/extensions/mqtt/__init__.py +++ /dev/null @@ -1,92 +0,0 @@ -""" - MQTT Extension - Includes interfaces for redis to - get data from events. -""" -import time -import paho.mqtt.client as mqtt -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'mqtt' - update_interval = 1 - - def init(self, config): - """ Prepare the mqtt connection and components """ - self.connections = {} - self.loop_started = False - - self.config = config - - if not isinstance(config, list): - config = [config] - - # Prepare clients for mqtt - for conf in config: - host = conf.get('host', 'localhost') - port = conf.get('port', 1883) - if conf['key'] not in self.connections: - self.connections[conf['key']] = {'client': None, - 'connected': False, - 'loop_started': False, - 'callbacks': {}} - - def on_conn(client, userdata, flags, rc): - if rc == 0: - self.connections[conf['key']]['connected'] = True - - self.connections[conf['key']]['client'] = mqtt.Client(f'mudpi-{conf["key"]}') - self.connections[conf['key']]['client'].on_connect = on_conn - self.connections[conf['key']]['client'].connect(host, port=port) - - while not self.connections[conf['key']]['connected']: - if not self.connections[conf['key']]['loop_started']: - self.connections[conf['key']]['client'].loop_start() - self.connections[conf['key']]['loop_started'] = True - time.sleep(0.1) - - return True - - def validate(self, config): - """ Validate the mqtt connection info """ - config = config[self.namespace] - if not isinstance(config, list): - config = [config] - - for conf in config: - key = conf.get('key') - if key is None: - raise ConfigError('MQTT missing a `key` in config for connection') - - host = conf.get('host') - if host is None: - conf['host'] = 'localhost' - - port = conf.get('port') - if port is None: - conf['port'] = 1883 - return config - - def unload(self): - """ Unload the extension """ - for conn in self.connections.values(): - conn['client'].loop_stop() - conn['client'].disconnect() - - def subscribe(self, key, topic, callback): - """ Listen on a topic and pass event data to callback """ - if topic not in self.connections[key]['callbacks']: - self.connections[key]['callbacks'][topic] = [callback] - else: - if callback not in self.connections[key]['callbacks'][topic]: - self.connections[key]['callbacks'][topic].append(callback) - - def callback_handler(client, userdata, message): - # log = f"{message.payload.decode()} {message.topic}" - if message.topic in self.connections[key]['callbacks']: - for callbk in self.connections[key]['callbacks'][message.topic]: - callbk(message.payload.decode("utf-8")) - - self.connections[key]['client'].on_message = callback_handler - return self.connections[key]['client'].subscribe(topic) \ No newline at end of file diff --git a/build/lib/mudpi/extensions/mqtt/sensor.py b/build/lib/mudpi/extensions/mqtt/sensor.py deleted file mode 100644 index 95a4399..0000000 --- a/build/lib/mudpi/extensions/mqtt/sensor.py +++ /dev/null @@ -1,149 +0,0 @@ -""" - MQTT Sensor Interface - Connects to a mqtt to get data - from an incoming event. -""" -import time -import json -from mudpi.utils import decode_event_data -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - # Override the update interval due to event handling - update_interval = 1 - - # Duration tracking - _duration_start = time.perf_counter() - - def load(self, config): - """ Load mqtt sensor component from configs """ - sensor = MQTTSensor(self.mudpi, config) - if sensor: - sensor.connect(self.extension) - self.add_component(sensor) - return True - - def validate(self, config): - """ Validate the mqtt sensor config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in MQTT sensor config.') - - expires = conf.get('expires') - if not expires: - conf['expires'] = 0 - else: - conf['expires'] = int(conf['expires']) - - return config - - -class MQTTSensor(Sensor): - """ MQTT Sensor - Returns a reading from events - """ - - # Track state change - _prev_state = None - - # Connection to mqtt - _conn = None - - # For duration tracking - _duration_start = time.perf_counter() - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return self.config.get('classifier', 'general') - - @property - def topic(self): - """ Return the topic to listen on for event sensors """ - return str(self.config.get('topic', f'sensor/{self.id}')) - - @property - def expires(self): - """ Return the time in which state becomes stale """ - return int(self.config.get('expires', 0)) - - @property - def expired(self): - """ Return if current data is expired """ - if self.expires > 0: - return time.perf_counter() - self._duration_start > self.expires - else: - return False - - - """ Methods """ - def init(self): - """ Connect to the device """ - # Perform inital state fetch - # self.update() - # self.store_state() - - return True - - def connect(self, extension): - """ Connect the sensor to mqtt """ - _conn_key = self.config['connection'] - self._conn = extension.connections[_conn_key]['client'] - extension.subscribe(_conn_key, self.topic, self.handle_event) - - - def update(self): - """ Get data from memory or wait for event """ - if self._conn: - if self.expired: - self.mudpi.events.publish('sensor', { - 'event': 'StateExpired', - 'component_id': self.id, - 'expires': self.expires, - 'previous_state': self.state, - 'type': self.type}) - self._state = None - if self._prev_state != self._state: - self.reset_duration() - self._prev_state = self._state - return self._state - - def handle_event(self, data={}): - """ Handle event from mqtt broker """ - if data is not None: - try: - # _event_data = self.last_event = decode_event_data(data) - self._state = data - except: - Logger.log( - LOG_LEVEL["info"], - f"Error Decoding Event for MQTT Sensor {self.id}" - ) - - def reset_duration(self): - """ Reset the duration of the current state """ - self._duration_start = time.perf_counter() - return True \ No newline at end of file diff --git a/build/lib/mudpi/extensions/nanpy/__init__.py b/build/lib/mudpi/extensions/nanpy/__init__.py deleted file mode 100644 index ff7a29b..0000000 --- a/build/lib/mudpi/extensions/nanpy/__init__.py +++ /dev/null @@ -1,202 +0,0 @@ -""" - Nanpy Extension - Allows arduino boards and ESP based devices - to be controlled via serial or wifi. -""" -import time -import random -import socket -import threading -from mudpi.workers import Worker -from mudpi.extensions import BaseExtension -from mudpi.exceptions import MudPiError, ConfigError -from nanpy import (ArduinoApi, SerialManager) -from mudpi.logger.Logger import Logger, LOG_LEVEL -from nanpy.serialmanager import SerialManagerError -from nanpy.sockconnection import (SocketManager, SocketManagerError) - -NAMESPACE = 'nanpy' - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = 30 - - def init(self, config): - """ Setup the nodes for components """ - self.config = config - self.nodes = {} - self.connections = {} - - if not isinstance(config, list): - config = [config] - - for conf in config: - key = conf.get('key') - if key not in self.nodes: - self.nodes[key] = Node(self.mudpi, conf) - - return True - - def validate(self, config): - """ Validate the Node configs """ - config = config[self.namespace] - if not isinstance(config, list): - config = [config] - - for conf in config: - key = conf.get('key') - if key is None: - raise ConfigError('Nanpy node missing a `key` in config') - - address = conf.get('address') - if address is None: - raise ConfigError('Nanpy node missing an `address` in config') - return config - - -class Node(Worker): - """ Worker to manage a node connection """ - def __init__(self, mudpi, config): - self.mudpi = mudpi - self.config = config - self.connection = None - self.api = None - - self._thread = None - self._lock = threading.Lock() - self._node_ready = threading.Event() - self._node_connected = threading.Event() - self._run_once = None - - self.mudpi.workers.register(self.key, self) - - @property - def key(self): - return self.config.get('key') - - @property - def ready(self): - """ Return if node is initialized """ - return self._node_ready.is_set() - - @property - def connected(self): - """ Return if node is connected to MudPi """ - return self._node_connected.is_set() - - def work(self, func=None): - # Node reconnection cycle - delay_multiplier = 1 - while self.mudpi.is_prepared: - if self.mudpi.is_running: - if not self._run_once: - self.connect() - self._run_once = True - if not self.connected: - # Random delay before connections to offset multiple attempts (1-5 min delay) - random_delay = (random.randrange(5, self.config.get( - "max_reconnect_delay", 60)) * delay_multiplier) / 2 - self._wait(10) - Logger.log_formatted(LOG_LEVEL["info"], - f'{self.key} -> Retrying Connection in {random_delay}s', 'Retrying', 'notice') - # Two separate checks for MudPi status to prevent re-connections during shutdown - if self.mudpi.is_running: - self._wait(random_delay) - if self.mudpi.is_running: - self.connection = self.connect() - if self.connection is None: - delay_multiplier += 1 - if delay_multiplier > 6: - delay_multiplier = 6 - else: - delay_multiplier = 1 - else: - time.sleep(1) - # MudPi Shutting Down, Perform Cleanup Below - Logger.log_formatted(LOG_LEVEL["debug"], - f"Worker {self.key} ", "Stopping", "notice") - self.connection.close() - self.reset_connection() - Logger.log_formatted(LOG_LEVEL["info"], - f"Worker {self.key} ", "Offline", "error") - - def connect(self): - """ Setup connection to a node over wifi or serial """ - if self.connected: - return True - - with self._lock: - # Check again if node connected while waiting on lock - if self.connected: - return True - - attempts = 3 - conn = None - if self.config.get('use_wifi', False): - while attempts > 0 and self.mudpi.is_running: - try: - Logger.log_formatted(LOG_LEVEL["debug"], - f'{self.config["name"]} -> Wifi ', 'Connecting', 'notice') - attempts -= 1 - conn = SocketManager( - host=str(self.config.get('address', 'mudpi-nanpy.local'))) - # Test the connection with api - self.api = ArduinoApi(connection=conn) - except (SocketManagerError, BrokenPipeError, ConnectionResetError, - socket.timeout) as e: - Logger.log_formatted(LOG_LEVEL["warning"], - f'{self.config["name"]} -> Failed Connection ', 'Timeout', 'notice') - if attempts > 0: - Logger.log_formatted(LOG_LEVEL["info"], - f'{self.config["name"]} -> Preparing Reconnect ', 'Pending', 'notice') - else: - Logger.log_formatted(LOG_LEVEL["error"], - f'{self.config["name"]} -> Connection Attempts ', 'Failed', 'error') - conn = None - self.reset_connection() - self._wait(5) - except (OSError, KeyError) as e: - Logger.log(LOG_LEVEL["error"], - f"[{self.config['name']}] Node Not Found. (Is it online?)") - conn = None - self.reset_connection() - self._wait(5) - else: - Logger.log_formatted(LOG_LEVEL["info"], - f"{self.config['name']} -> Wifi Connection ", 'Connected', 'success') - break - else: - while attempts > 0 and self.mudpi.is_running: - try: - attempts -= 1 - conn = SerialManager(device=str(self.config.get('address', '/dev/ttyUSB1'))) - self.api = ArduinoApi(connection=conn) - except SerialManagerError: - Logger.log_formatted(LOG_LEVEL["warning"], - f"{self.config['name']} -> Connecting ", 'Timeout', 'notice') - if attempts > 0: - Logger.log_formatted(LOG_LEVEL["info"], - f'{self.config["name"]} -> Preparing Reconnect ', 'Pending', 'notice') - else: - Logger.log_formatted(LOG_LEVEL["error"], - f'{self.config["name"]} -> Connection Attempts ', 'Failed', 'error') - self.reset_connection() - conn = None - self._wait(5) - else: - if conn is not None: - Logger.log_formatted(LOG_LEVEL["info"], - f'[{self.config["name"]}] -> Serial Connection ', 'Connected', 'success') - break - if conn is not None: - self.connection = conn - self._node_connected.set() - self._node_ready.set() - return conn - - def reset_connection(self): - """ Reset the connection """ - self.connection = None - self._node_connected.clear() - self._node_ready.clear() - return True diff --git a/build/lib/mudpi/extensions/nanpy/char_display.py b/build/lib/mudpi/extensions/nanpy/char_display.py deleted file mode 100644 index 5d97efb..0000000 --- a/build/lib/mudpi/extensions/nanpy/char_display.py +++ /dev/null @@ -1,149 +0,0 @@ -""" - Nanpy LCD Display Interface - Connects to a unit running Nanpy to - get display messages on an LCD. -""" -import socket -from nanpy.lcd import Lcd -from nanpy.lcd_i2c import Lcd_I2C -from mudpi.extensions import BaseInterface -from nanpy import (ArduinoApi, SerialManager) -from mudpi.logger.Logger import Logger, LOG_LEVEL -from nanpy.serialmanager import SerialManagerError -from mudpi.exceptions import MudPiError, ConfigError -from mudpi.extensions.char_display import CharDisplay -from nanpy.sockconnection import (SocketManager, SocketManagerError) - - -class Interface(BaseInterface): - def load(self, config): - """ Load Nanpy display components from configs """ - display = NanpyCharDisplay(self.mudpi, config) - - if display: - node = self.extension.nodes[config['node']] - if node: - display.node = node - self.add_component(display) - else: - raise MudPiError(f'Nanpy node {config["node"]} not found trying to connect {config["key"]}.') - return True - - def validate(self, config): - """ Validate the Nanpy display config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in Nanpy sensor config.') - - if not conf.get('node'): - raise ConfigError(f'Missing `node` in Nanpy sensor {conf["key"]} config. You need to add a node key.') - - if not conf.get('address'): - # raise ConfigError('Missing `address` in i2c char_lcd config.') - conf['address'] = 0x27 - else: - addr = conf['address'] - - # Convert hex string/int to actual hex - if isinstance(addr, str): - addr = hex(int(addr, 39)) - elif isinstance(addr, int): - addr = hex(addr) - - conf['address'] = addr - - if not isinstance conf.get('columns', 16): - raise ConfigError('Missing `columns` must be an int.') - - if not isinstance conf.get('rows', 2): - raise ConfigError('Missing `rows` must be an int.') - - if conf['type'].lower() not in ('gpio', 'i2c'): - conf['type'] = 'i2c' - - return config - - -class NanpyCharDisplay(CharDisplay): - """ Nanpy Character Display - Display messages on a LCD connected to i2c - """ - - """ Properties """ - @property - def address(self): - """ Unique id or key """ - return self.config.get('address', 0x27) - - @property - def type(self): - """ Returns if using i2c or gpio for connection """ - return self.config.get('type', 'i2c').lower() - - - """ Methods """ - def init(self): - """ Connect to the Parent Device """ - self._state = None - self._lcd = None - return True - - def check_connection(self): - """ Check connection to node and lcd """ - if self.node.connected: - if not self._lcd: - if self.type == 'i2c' - # PCF I2C Backpack - # [lcd_Addr, enable, Rw, Rs, d4, d5, d6, d7, backlighPin, pol] - pins = [0x27, 2, 1, 0, 4, 5, 6, 7, 3, 0] - # TODO: Add other backback support - self._lcd = Lcd_I2C(pins, [self.columns, self.rows], connection=self.node.connection) - else: - # GPIO Connection - # [rs, enable, d4, d5, d6, d7] - pins = [self.config.get('rs_pin', 7), - self.config.get('enable_pin', 8), - self.config.get('pin_1', 9), - self.config.get('pin_2', 10), - self.config.get('pin_3', 11), - self.config.get('pin_4', 12)] - self._lcd = Lcd(pins, [self.columns, self.rows], connection=connection) - - def update(self): - """ Control LCD display nanpy""" - if self.node.connected: - try: - super().update() - except (SerialManagerError, SocketManagerError, - BrokenPipeError, ConnectionResetError, OSError, - socket.timeout) as e: - if self.node.connected: - Logger.log_formatted(LOG_LEVEL["warning"], - f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') - self.node.reset_connection() - return None - - """ Actions """ - def clear(self, data=None): - """ Clear the display screen """ - self._lcd.clear() - Logger.log(LOG_LEVEL["debug"], 'Cleared the LCD Screen') - - def show(self, data={}): - """ Show a message on the display """ - if not isinstance(data, dict): - data = {'message': data} - - self._lcd.setCursor(0, 0) - self._lcd.printString(data.get('message', '')) - - def turn_on_backlight(self): - """ Turn the backlight on """ - self._lcd.setBacklight(0) - - def turn_off_backlight(self): - """ Turn the backlight on """ - self._lcd.setBacklight(1) \ No newline at end of file diff --git a/build/lib/mudpi/extensions/nanpy/control.py b/build/lib/mudpi/extensions/nanpy/control.py deleted file mode 100644 index 2eefa49..0000000 --- a/build/lib/mudpi/extensions/nanpy/control.py +++ /dev/null @@ -1,151 +0,0 @@ -""" - Nanpy Control Interface - Connects to a unit running Nanpy to - read a switch, button or potentiometer. -""" -import time -import socket -from mudpi.extensions import BaseInterface -from mudpi.extensions.control import Control -from nanpy import (ArduinoApi, SerialManager, DHT) -from mudpi.logger.Logger import Logger, LOG_LEVEL -from nanpy.serialmanager import SerialManagerError -from mudpi.exceptions import MudPiError, ConfigError -from nanpy.sockconnection import (SocketManager, SocketManagerError) - -UPDATE_THROTTLE = 2 -DEBOUNCE = 0.05 - -class Interface(BaseInterface): - - def load(self, config): - """ Load Nanpy control components from configs """ - control = NanpyGPIOControl(self.mudpi, config) - if control: - node = self.extension.nodes[config['node']] - if node: - control.node = node - self.add_component(control) - else: - raise MudPiError(f'Nanpy node {config["node"]} not found trying to connect {config["key"]}.') - return True - - def validate(self, config): - """ Validate the Nanpy control config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in Nanpy sensor config.') - - if not conf.get('node'): - raise ConfigError(f'Missing `node` in Nanpy sensor {conf["key"]} config. You need to add a node key.') - - if conf.get('pin') is None: - raise ConfigError(f'Missing `pin` in Nanpy control {conf["key"]} config. You need to add a pin.') - - if not conf.get('type'): - # Default to the button type - conf['type'] = 'button' - elif conf.get('type').lower() not in ['button', 'switch', 'potentiometer']: - # Unsupported type - conf['type'] = 'button' - - return config - - -class NanpyGPIOControl(Control): - """ Nanpy GPIO Control - Get readings from GPIO (analog or digital) - """ - - """ Properties """ - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def analog(self): - """ Return if gpio is digital or analog """ - return self.type == 'potentiometer' - - @property - def buffer(self): - """ Reading buffer to smooth out jumpy values """ - return self.config.get('buffer', 0) - - @property - def elapsed_time(self): - self.time_elapsed = time.perf_counter() - self.time_start - return self.time_elapsed - - """ Methods """ - def init(self): - """ Connect to the Parent Device """ - self._state = 0 - self.reset_elapsed_time() - self._fired = False - self._pin_setup = False - self.previous_state = 0 - return True - - def check_connection(self): - """ Verify node connection and devices setup """ - if self.node.connected: - if not self._pin_setup: - self.node.api.pinMode(self.pin, self.node.api.INPUT) - self._pin_setup = True - - def update(self): - """ Get data from GPIO through nanpy""" - if self.node.connected: - self.check_connection() - try: - data = self.node.api.analogRead(self.pin) if self.analog else self.node.api.digitalRead(self.pin) - if self.type != 'potentiometer': - self.previous_state = self._state - self._state = data - else: - if (data < self.previous_state - self.buffer) or (data > self.previous_state + self.buffer): - self.previous_state = self._state - self._state = data - self.handle_state() - except (SerialManagerError, SocketManagerError, - BrokenPipeError, ConnectionResetError, OSError, - socket.timeout) as e: - if self.node.connected: - Logger.log_formatted(LOG_LEVEL["warning"], - f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') - self.node.reset_connection() - self._pin_setup = False - return None - - def handle_state(self): - """ Control logic depending on type of control """ - if self.type == 'button': - if self._state: - if not self.invert_state: - if self.elapsed_time > UPDATE_THROTTLE: - self.fire() - self.reset_elapsed_time() - else: - if self.invert_state: - if self.elapsed_time > UPDATE_THROTTLE: - self.fire() - self.reset_elapsed_time() - elif self.type == 'switch': - if self._state == self.previous_state: - if self.elapsed_time > DEBOUNCE and not self._fired: - self.fire() - self._fired = True - self.reset_elapsed_time() - else: - self._fired = False - elif self.type == 'potentiometer': - if self._state != self.previous_state: - self.fire() - - def reset_elapsed_time(self): - self.time_start = time.perf_counter() \ No newline at end of file diff --git a/build/lib/mudpi/extensions/nanpy/sensor.py b/build/lib/mudpi/extensions/nanpy/sensor.py deleted file mode 100644 index ffcbad7..0000000 --- a/build/lib/mudpi/extensions/nanpy/sensor.py +++ /dev/null @@ -1,225 +0,0 @@ -""" - Nanpy Sensor Interface - Connects to a unit running Nanpy to - get readings from the device. -""" -import socket -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor -from nanpy import (ArduinoApi, SerialManager, DHT) -from mudpi.logger.Logger import Logger, LOG_LEVEL -from nanpy.serialmanager import SerialManagerError -from mudpi.exceptions import MudPiError, ConfigError -from nanpy.sockconnection import (SocketManager, SocketManagerError) - - -class Interface(BaseInterface): - def load(self, config): - """ Load Nanpy sensor components from configs """ - sensor = None - if config['type'].lower() == 'gpio': - sensor = NanpyGPIOSensor(self.mudpi, config) - elif config['type'].lower() == 'dht': - sensor = NanpyDHTSensor(self.mudpi, config) - elif config['type'].lower() == 'dallas_temperature': - # sensor = OneWireSensor(self.mudpi, config) NOT IMPLMENTED YET - pass - - if sensor: - node = self.extension.nodes[config['node']] - if node: - sensor.node = node - self.add_component(sensor) - else: - raise MudPiError(f'Nanpy node {config["node"]} not found trying to connect {config["key"]}.') - return True - - def validate(self, config): - """ Validate the Nanpy sensor config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in Nanpy sensor config.') - - if not conf.get('node'): - raise ConfigError(f'Missing `node` in Nanpy sensor {conf["key"]} config. You need to add a node key.') - - if not conf.get('type'): - # Default the sensor type - conf['type'] = 'gpio' - - if conf['type'].lower() == 'gpio': - pin = conf.get('pin') - if not pin and pin != 0: - raise ConfigError(f'Missing `pin` in Nanpy gpio sensor {conf["key"]} config.') - elif conf['type'].lower() == 'dht': - pin = conf.get('pin') - if not pin and pin != 0: - raise ConfigError(f'Missing `pin` in Nanpy dht sensor {conf["key"]} config.') - - if not conf.get('model'): - # Default DHT Model - conf['model'] = '11' - else: - # Defaulting the model to DHT11 - if conf['model'] not in NanpyDHTSensor.models: - conf['model'] = '11' - - conf['classifier'] = 'climate' - elif conf['type'].lower() == 'dallas_temperature': - if not conf.get('address'): - # raise ConfigError(f'Missing `address` in Nanpy dallas_temperature sensor {conf['key']} config.') - pass - conf['classifier'] = 'temperature' - - if not conf.get('classifier'): - # Default the sensor classifier - conf['classifier'] = 'general' - - return config - - -class NanpyGPIOSensor(Sensor): - """ Nanpy GPIO Sensor - Get readings from GPIO (analog or digital) - """ - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return 'climate' - - @property - def analog(self): - """ Return if gpio is digital or analog """ - return self.config.get('analog', False) - - @property - def pin(self): - """ Return if gpio is digital or analog """ - return self.config.get('pin') - - """ Methods """ - def init(self): - """ Connect to the Parent Device """ - self._state = None - return True - - def update(self): - """ Get data from GPIO through nanpy""" - if self.node.connected: - try: - data = None - if self.analog: - data = self.node.api.analogRead(self.pin) - else: - data = self.node.api.digitalRead(self.pin) - self._state = data - except (SerialManagerError, SocketManagerError, - BrokenPipeError, ConnectionResetError, OSError, - socket.timeout) as e: - if self.node.connected: - Logger.log_formatted(LOG_LEVEL["warning"], - f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') - self.node.reset_connection() - return None - -class NanpyDHTSensor(Sensor): - """ Nanpy DHT Sensor - Get readings from DHT device. - """ - models = { '11': DHT.DHT11, - '22': DHT.DHT22, - '2301': DHT.AM2301 } - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return self.config.get('classifier', 'general') - - @property - def analog(self): - """ Return if gpio is digital or analog """ - return self.config.get('analog', False) - - @property - def pin(self): - """ Return if gpio is digital or analog """ - return self.config.get('pin') - - @property - def model(self): - """ Return DHT model """ - if self.config.get('model') not in NanpyDHTSensor.models: - conf['model'] = '11' - return self.models[self.config.get('model', '11')] - - """ Methods """ - def init(self): - """ Connect to the Parent Device """ - self._state = None - self._dht = None - # Attribute to track from DHT device - self._attribute = self.config.get('attribute', 'temperature') - return True - - def check_connection(self): - """ Check connection to node and DHT """ - if self.node.connected: - if not self._dht: - self._dht = DHT(self.pin, self.model, connection=self.node.connection) - - def update(self): - """ Get data from DHT through nanpy""" - if self.node.connected: - try: - self.check_connection() - if self._dht: - temperature = self._dht.readTemperature(True) - humidity = self._dht.readHumidity() - data = {'temperature': round(temperature, 2), - 'humidity': round(humidity, 2)} - self._state = data - except (SerialManagerError, SocketManagerError, - BrokenPipeError, ConnectionResetError, OSError, - socket.timeout) as e: - if self.node.connected: - Logger.log_formatted(LOG_LEVEL["warning"], - f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') - self.node.reset_connection() - self._dht = None - return None \ No newline at end of file diff --git a/build/lib/mudpi/extensions/nanpy/toggle.py b/build/lib/mudpi/extensions/nanpy/toggle.py deleted file mode 100644 index ecd48e8..0000000 --- a/build/lib/mudpi/extensions/nanpy/toggle.py +++ /dev/null @@ -1,139 +0,0 @@ -""" - Nanpy Toggle Interface - Connects to a unit running Nanpy to - toggle a GPIO pin on the device. -""" -import socket -from mudpi.extensions import BaseInterface -from mudpi.extensions.toggle import Toggle -from nanpy import (ArduinoApi, SerialManager, DHT) -from mudpi.logger.Logger import Logger, LOG_LEVEL -from nanpy.serialmanager import SerialManagerError -from mudpi.exceptions import MudPiError, ConfigError -from nanpy.sockconnection import (SocketManager, SocketManagerError) - -UPDATE_THROTTLE = 2 - -class Interface(BaseInterface): - - def load(self, config): - """ Load Nanpy Toggle components from configs """ - toggle = NanpyGPIOToggle(self.mudpi, config) - if toggle: - node = self.extension.nodes[config['node']] - if node: - toggle.node = node - self.add_component(toggle) - else: - raise MudPiError(f'Nanpy node {config["node"]} not found trying to connect {config["key"]}.') - return True - - def validate(self, config): - """ Validate the Nanpy control config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in Nanpy sensor config.') - - if not conf.get('node'): - raise ConfigError(f'Missing `node` in Nanpy sensor {conf["key"]} config. You need to add a node key.') - - if not conf.get('pin'): - raise ConfigError(f'Missing `pin` in Nanpy toggle {conf["key"]} config. You need to add a pin.') - - return config - - -class NanpyGPIOToggle(Toggle): - """ Nanpy GPIO Toggle - Get readings from GPIO (analog or digital) - """ - - """ Properties """ - @property - def pin(self): - """ The GPIO pin """ - return self.config.get('pin') - - - """ Methods """ - def init(self): - """ Connect to the Parent Device """ - self._pin_setup = False - self.active = False - return True - - def check_connection(self): - """ Verify node connection and devices setup """ - if self.node.connected: - if not self._pin_setup: - self.node.api.pinMode(self.pin, self.node.api.OUTPUT) - if self.invert_state: - self.pin_state_on = self.node.api.LOW - self.pin_state_off = self.node.api.HIGH - else: - self.pin_state_on = self.node.api.HIGH - self.pin_state_off = self.node.api.LOW - self.node.api.digitalWrite(self.pin, self.pin_state_off) - self._pin_setup = True - - def update(self): - """ Wrap the failsafe detection in connection handler for node """ - if self.node.connected: - self.check_connection() - try: - # Pass to the Base Toggle for failsafe handling - super().update() - except (SerialManagerError, SocketManagerError, - BrokenPipeError, ConnectionResetError, OSError, - socket.timeout) as e: - if self.node.connected: - Logger.log_formatted(LOG_LEVEL["warning"], - f'{self.node.key} -> Broken Connection', 'Timeout', 'notice') - self.node.reset_connection() - self._pin_setup = False - return None - - def restore_state(self, state={}): - """ This is called on start to - restore previous state """ - if self._pin_setup: - state = self.pin_state_on if state.get('state', False) else self.pin_state_off - self.node.api.digitalWrite(self.pin, state) - return - - - """ Actions """ - def toggle(self, data={}): - # Toggle the GPIO state - if self.mudpi.is_prepared: - # Do inverted check and change value before setting active - # to avoid false state being provided in the event fired. - if self.node.connected: - self.check_connection() - state = self.pin_state_on if not self.active else self.pin_state_off - self.node.api.digitalWrite(self.pin, state) - self.active = not self.active - self.store_state() - - def turn_on(self, data={}): - # Turn on GPIO if its not on - if self.mudpi.is_prepared: - if not self.active: - if self.node.connected: - self.check_connection() - self.node.api.digitalWrite(self.pin, self.pin_state_on) - self.active = True - self.store_state() - - def turn_off(self, data={}): - # Turn off GPIO if its not off - if self.mudpi.is_prepared: - if self.active: - if self.node.connected: - self.check_connection() - self.node.api.digitalWrite(self.pin, self.pin_state_off) - self.active = False - self.store_state() \ No newline at end of file diff --git a/build/lib/mudpi/extensions/picamera/__init__.py b/build/lib/mudpi/extensions/picamera/__init__.py deleted file mode 100644 index 5e0789d..0000000 --- a/build/lib/mudpi/extensions/picamera/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" - Raspberry Pi Camera Extension - Connect to a raspberry pi camera - through the picamera library. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'picamera' - update_interval = 1 - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/picamera/camera.py b/build/lib/mudpi/extensions/picamera/camera.py deleted file mode 100644 index bcd8b26..0000000 --- a/build/lib/mudpi/extensions/picamera/camera.py +++ /dev/null @@ -1,101 +0,0 @@ -""" - Picamera Interface - Connects to a raspberry pi camera - through the picamera library. -""" -from picamera import PiCamera -from mudpi.utils import decode_event_data -from mudpi.exceptions import ConfigError -from mudpi.extensions import BaseInterface -from mudpi.extensions.camera import Camera -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class Interface(BaseInterface): - - def load(self, config): - """ Load pi camera component from configs """ - camera = RaspberryPiCamera(self.mudpi, config) - if camera: - self.add_component(camera) - return True - - def validate(self, config): - """ Validate the camera config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('path'): - raise ConfigError('Camera needs a `path` to save files to.') - - return config - - -class RaspberryPiCamera(Camera): - """ Base Camera - Base Camera for all camera interfaces - """ - - # Pi camera object - camera = None - - """ Properties """ - @property - def record_video(self): - """ Set to True to record video instead of photos """ - return self.config.get('record_video', False) - - - """ Methods """ - def init(self): - """ Prepare the Picamera """ - self.camera = PiCamera( - resolution=(self.width, self.height)) - # Below we calibrate the camera for consistent imaging - self.camera.framerate = self.framerate - # 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 - - def update(self): - """ Main update loop to check when to capture images """ - if self.mudpi.is_prepared: - if self.duration > self.delay.total_seconds(): - if self.record_video: - self.capture_recording(duration=self.record_duration) - else: - self.capture_image() - print(f'Camera {self.id} time:{self.duration}') - self.reset_duration() - - - """ Actions """ - def capture_image(self, data={}): - """ Capture a single image from the camera - it should use the file name and increment - counter for sequenetial images """ - if self.camera: - image_name = f'{os.path.join(self.path, self.filename)}.jpg' - self.camera.capture(image_name) - self.last_image = os.path.abspath(image_name) - self.increment_count() - self.fire({'event': 'ImageCaptured', 'image': image_name}) - - def capture_recording(self, data={}: - """ Record a video from the camera """ - _duration = data.get('duration', 5) - if self.camera: - _file_name = f'{os.path.join(self.path, self.filename)}.h264' - self.camera.start_recording(_file_name) - self.camera.wait_recording(_duration) - self.camera.stop_recording() - self.last_image = os.path.abspath(_file_name) - self.increment_count() - self.fire({'event': 'RecordingCaptured', 'file': _file_name}) - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/redis/__init__.py b/build/lib/mudpi/extensions/redis/__init__.py deleted file mode 100644 index 40f14f1..0000000 --- a/build/lib/mudpi/extensions/redis/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -""" - Redis Extension - Includes interfaces for redis to - get data and manage events. -""" -import redis -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'redis' - update_interval = 30 - - def init(self, config): - """ Prepare the redis connection and components """ - self.connections = {} - self.config = config - - if not isinstance(config, list): - config = [config] - - # Prepare connections to redis - for conf in config: - host = conf.get('host', '127.0.0.1') - port = conf.get('port', 6379) - if conf['key'] not in self.connections: - self.connections[conf['key']] = redis.Redis(host=host, port=port) - - return True - - def validate(self, config): - """ Validate the redis connection info """ - config = config[self.namespace] - if not isinstance(config, list): - config = [config] - - for conf in config: - key = conf.get('key') - if key is None: - raise ConfigError('Redis missing a `key` in config for connection') - - host = conf.get('host') - if host is None: - conf['host'] = '127.0.0.1' - - port = conf.get('port') - if port is None: - conf['port'] = 6379 - return config \ No newline at end of file diff --git a/build/lib/mudpi/extensions/redis/sensor.py b/build/lib/mudpi/extensions/redis/sensor.py deleted file mode 100644 index 07995f2..0000000 --- a/build/lib/mudpi/extensions/redis/sensor.py +++ /dev/null @@ -1,176 +0,0 @@ -""" - Redis Sensor Interface - Connects to a redis to get data - from state or event. -""" -import time -import json -import redis -from mudpi.utils import decode_event_data -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - # Override the update interval due to event handling - update_interval = 1 - - # Duration tracking - _duration_start = time.perf_counter() - - def load(self, config): - """ Load redis sensor component from configs """ - sensor = RedisSensor(self.mudpi, config) - if sensor: - sensor.connect(self.extension.connections[config['connection']]) - self.add_component(sensor) - return True - - def validate(self, config): - """ Validate the redis sensor config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in redis sensor config.') - - if not conf.get('type'): - # Default the sensor type - conf['type'] = 'state' - - if conf['type'].lower() == 'state': - state_key = conf.get('state_key') - if not state_key: - pass - # raise ConfigError(f'Missing `state_key` for `state` redis sensor type {conf["key"]} config.') - elif conf['type'].lower() == 'event': - expires = conf.get('expires') - if not expires: - conf['expires'] = 0 - else: - conf['expires'] = int(conf['expires']) - - return config - - -class RedisSensor(Sensor): - """ Redis Sensor - Returns a reading from redis state or events - """ - - # Track state change - _prev_state = None - - # Connection to redis - _conn = None - - # For duration tracking - _duration_start = time.perf_counter() - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return self.config.get('classifier', 'general') - - @property - def type(self): - """ Return the sensor type (event, state) """ - return self.config.get('type', 'state').lower() - - @property - def topic(self): - """ Return the topic to listen on for event sensors """ - return str(self.config.get('topic', f'sensor/{self.id}')) - - @property - def state_key(self): - """ Return the key to get from redis for state sensor """ - return self.config.get('state_key', self.id) - - @property - def expires(self): - """ Return the time in which state becomes stale """ - return int(self.config.get('expires', 0)) - - @property - def expired(self): - """ Return if current data is expired """ - if self.expires > 0: - return time.perf_counter() - self._duration_start > self.expires - else: - return False - - - """ Methods """ - def init(self): - """ Connect to the device """ - # Perform inital state fetch - # self.update() - # self.store_state() - - return True - - def connect(self, connection): - """ Connect the sensor to redis """ - self._conn = connection - if self.type == 'event': - self.bus = self._conn.pubsub() - self.bus.subscribe(**{self.topic: self.handle_event}) - - def update(self): - """ Get data from memory or wait for event """ - if self._conn: - if self.type == 'state': - _data = self._conn.get(self.state_key) - self._state = _data.decode('utf-8') - else: - self.bus.get_message() - if self.expired: - self.mudpi.events.publish('sensor', { - 'event': 'StateExpired', - 'component_id': self.id, - 'expires': self.expires, - 'previous_state': self.state, - 'type': self.type}) - self._state = None - if self._prev_state != self._state: - self.reset_duration() - self._prev_state = self._state - return self._state - - def handle_event(self, event={}): - """ Handle event from redis pubsub """ - data = decode_event_data(event['data']) - if data is not None: - try: - # _event_data = self.last_event = decode_event_data(data) - self._state = data - except: - Logger.log( - LOG_LEVEL["info"], - f"Error Decoding Event for Redis Sensor {self.id}" - ) - - def reset_duration(self): - """ Reset the duration of the current state """ - self._duration_start = time.perf_counter() - return True \ No newline at end of file diff --git a/build/lib/mudpi/extensions/rtsp/__init__.py b/build/lib/mudpi/extensions/rtsp/__init__.py deleted file mode 100644 index 93239d4..0000000 --- a/build/lib/mudpi/extensions/rtsp/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" - RTSP Camera Extension - Connects to a RTSP or HTTP camera - stream to capture images and record - videos. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'rtsp' - update_interval = 1 - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/rtsp/camera.py b/build/lib/mudpi/extensions/rtsp/camera.py deleted file mode 100644 index 32367c4..0000000 --- a/build/lib/mudpi/extensions/rtsp/camera.py +++ /dev/null @@ -1,177 +0,0 @@ -""" - RTSP Camera Interface - Connects to a camera rtsp stream - through the opencv library. -""" -import os -import cv2 -import time -from mudpi.utils import decode_event_data -from mudpi.exceptions import ConfigError -from mudpi.extensions import BaseInterface -from mudpi.extensions.camera import Camera -from mudpi.logger.Logger import Logger, LOG_LEVEL - -# Minimum time inbetween updates -MIN_UPDATE_INTERVAL = 5 - - -class Interface(BaseInterface): - - update_interval = 1 - - def load(self, config): - """ Load rtsp camera component from configs """ - camera = RTSPCamera(self.mudpi, config) - if camera: - self.add_component(camera) - return True - - def validate(self, config): - """ Validate the camera config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('path'): - raise ConfigError('Camera needs a `path` to save files to.') - - if not conf.get('source'): - raise ConfigError('RTSP Camera needs a `source` to stream from.') - - _delay = conf.get('delay', {}) - if _delay: - h = _delay.get('hours', 0) - m = _delay.get('minutes', 0) - s = _delay.get('seconds', 0) - if h == 0 and m == 0 and s < MIN_UPDATE_INTERVAL: - raise ConfigError('RTSP Camera minimum `delay` must be 5 seconds.') - - return config - - -class RTSPCamera(Camera): - """ RTSP Camera - Camera connected over RTSP stream - """ - - # Video capture object - cap = None - - # Resolution tuple for capture - size = None - - # Resolution tuple for capture - _detected_size = None - - # Value to scale image to target size - _scale = None - - # Number of recordings saved - _rec_count = 0 - - """ Properties """ - @property - def source(self): - """ State is the last image for display """ - return self.config.get('source', 0) - - @property - def record_video(self): - """ Set to True to record video instead of photos """ - return self.config.get('record_video', False) - - """ Methods """ - def update(self): - """ Main update loop to check when to capture images """ - if self.mudpi.is_prepared: - if self.duration > self.delay.total_seconds(): - if self.record_video: - self.capture_recording(duration=self.record_duration) - else: - self.capture_image() - print(f'Camera {self.id} time:{self.duration}') - self.reset_duration() - - def open_stream(self): - """ Open the capture stream """ - if not self.cap: - self.cap = cv2.VideoCapture(str(self.source)) # it can be rtsp or http stream - - self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width) - self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height) - - if not self._detected_size: - self._detected_size = (int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)), - int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))) - - if not self.size: - if self._detected_size[0] > 0: - self._scale = (self._detected_size[0] - (self._detected_size[0] - self.width)) / self._detected_size[0] - self.size = (int(self._detected_size[0] * self._scale), int(self._detected_size[1] * self._scale)) - - if not self.cap.isOpened(): - self.cap.open(str(self.source)) - - - """ Actions """ - def capture_image(self, data={}): - """ Capture a single image from the camera """ - self.check_path() - self.open_stream() - - if self.cap.isOpened(): - _,frame = self.cap.read() - self.cap.release() #releasing camera immediately after capturing picture - if _ and frame is not None: - image_name = f'{os.path.join(self.path, self.filename)}.jpg' - frame = cv2.resize(frame, self.size, interpolation = cv2.INTER_AREA) - cv2.imwrite(image_name, frame) - self.last_image = os.path.abspath(image_name) - self.increment_count() - self.fire({'event': 'ImageCaptured', 'image': image_name}) - # cap.release() - - def capture_recording(self, data={}): - """ Record a video from the camera """ - self.check_path() - self.open_stream() - _duration = data.get('duration', 5) - - self.fourcc = cv2.VideoWriter_fourcc(*'MP4V') - _file_name = f'{os.path.join(self.path, self.filename)}.mp4' - print(_file_name) - _writer = cv2.VideoWriter(_file_name, self.fourcc, self.framerate, self.size) - - _start = time.perf_counter() - while self.cap.isOpened(): - ret, frame = self.cap.read() - if ret and frame is not None: - frame = cv2.resize(frame, self.size, interpolation = cv2.INTER_AREA) - # saving recording - _writer.write(frame) - - if time.perf_counter() - _start > _duration: - break - self.cap.release() - _writer.release() - self.last_image = os.path.abspath(_file_name) - self.increment_count() - self.fire({'event': 'RecordingCaptured', 'file': _file_name}) - - def unload(self): - """ Cleanup resources """ - if self.cap: - self.cap.release() - - -class CameraStream: - """ A class for camera stream to run - get images and record videos from. - """ - - # TODO: Make this run a worker for faster - # image processing and recording. Fix the - # fps to match the desired duration. Keep - # all these camera resources seperate from - # the component to make log more central. \ No newline at end of file diff --git a/build/lib/mudpi/extensions/sensor/__init__.py b/build/lib/mudpi/extensions/sensor/__init__.py deleted file mode 100644 index 233d373..0000000 --- a/build/lib/mudpi/extensions/sensor/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -""" - Sensors Extension - Sensors are components that gather data and make it - available to MudPi. Sensors support interfaces to - allow additions of new types of devices easily. -""" -from mudpi.extensions import Component, BaseExtension - - -NAMESPACE = 'sensor' -UPDATE_INTERVAL = 30 - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = UPDATE_INTERVAL - - def init(self, config): - self.config = config[self.namespace] - - self.manager.init(self.config) - - self.manager.register_component_actions('force_update', action='force_update') - return True - - - -class Sensor(Component): - """ Base Sensor - Base Sensor for all sensor interfaces - """ - - """ Properties """ - @property - def id(self): - """ Unique id or key """ - return self.config.get('key') - - @property - def name(self): - """ Friendly name of control """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - - """ Actions """ - def force_update(self, data=None): - """ Force an update of the component. Useful for testing """ - self.update() - self.store_state() - return True \ No newline at end of file diff --git a/build/lib/mudpi/extensions/sensor/trigger.py b/build/lib/mudpi/extensions/sensor/trigger.py deleted file mode 100644 index 6d01597..0000000 --- a/build/lib/mudpi/extensions/sensor/trigger.py +++ /dev/null @@ -1,95 +0,0 @@ -""" - Sensor Trigger Interface - Monitors sensor state changes and - checks new state against any - thresholds if provided. -""" -import json -from mudpi.utils import decode_event_data -from mudpi.exceptions import ConfigError -from mudpi.extensions import BaseInterface -from mudpi.extensions.trigger import Trigger -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class Interface(BaseInterface): - - def load(self, config): - """ Load sensor Trigger component from configs """ - trigger = SensorTrigger(self.mudpi, config) - if trigger: - self.add_component(trigger) - return True - - def validate(self, config): - """ Validate the trigger config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('source'): - raise ConfigError('Missing `source` key in Sensor Trigger config.') - - return config - - -class SensorTrigger(Trigger): - """ A trigger that listens to sensor - states and checks for new state that - matches any thresholds. - """ - - # Used for onetime subscribe - _listening = False - - - """ Methods """ - def init(self): - """ Listen to the sensors state for changes """ - super().init() - if self.mudpi.is_prepared: - if not self._listening: - # TODO: Eventually get a handler returned to unsub just this listener - self.mudpi.events.subscribe('state', self.handle_event) - self._listening = True - return True - - def handle_event(self, event): - """ Handle the event data from the event system """ - _event_data = decode_event_data(event) - if _event_data.get('event'): - try: - if _event_data['event'] == 'StateUpdated': - if _event_data['component_id'] == self.source: - sensor_value = self._parse_data(_event_data["new_state"]["state"]) - if self.evaluate_thresholds(sensor_value): - self.active = True - if self._previous_state != self.active: - # Trigger is reset, Fire - self.trigger(_event_data) - else: - # Trigger not reset check if its multi fire - if self.frequency == 'many': - self.trigger(_event_data) - else: - self.active = False - except Exception as error: - Logger.log(LOG_LEVEL["error"], - f'Error evaluating thresholds for trigger {self.id}') - Logger.log(LOG_LEVEL["debug"], error) - self._previous_state = self.active - - def unload(self): - # Unsubscribe once bus supports single handler unsubscribes - return - - def _parse_data(self, data): - """ Get nested data if set otherwise return the data """ - try: - data = json.loads(data) - except Exception as error: - pass - if isinstance(data, dict): - return data if not self.nested_source else data.get(self.nested_source, None) - return data - diff --git a/build/lib/mudpi/extensions/sequence/__init__.py b/build/lib/mudpi/extensions/sequence/__init__.py deleted file mode 100644 index 45a4dd8..0000000 --- a/build/lib/mudpi/extensions/sequence/__init__.py +++ /dev/null @@ -1,481 +0,0 @@ -""" - Sequence Extension - Enabled actions to be grouped into - automations that can be fired in - a sequenctial order. -""" -import json -import time -import datetime -import threading -from mudpi.utils import decode_event_data -from mudpi.constants import FONT_RESET, FONT_CYAN -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.extensions import BaseExtension, Component -from mudpi.exceptions import MudPiError, ConfigError - -NAMESPACE = 'sequence' - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = 0.05 - - def init(self, config): - self.config = config - - if not isinstance(config, list): - config = [config] - - for conf in config: - sequence = Sequence(self.mudpi, conf) - if sequence: - self.manager.add_component(sequence) - self.manager.register_component_actions('start', action='start') - self.manager.register_component_actions('stop', action='stop') - self.manager.register_component_actions('previous_step', action='previous_step') - self.manager.register_component_actions('reset_step', action='reset_step') - self.manager.register_component_actions('next_step', action='advance_step') - self.manager.register_component_actions('skip_step', action='skip_step') - self.manager.register_component_actions('reset', action='reset') - self.manager.register_component_actions('restart', action='restart') - return True - - def validate(self, config): - """ Validate the Sequence configs """ - config = config[self.namespace] - if not isinstance(config, list): - config = [config] - - for conf in config: - key = conf.get('key') - if key is None: - raise ConfigError('Seqeunce missing a `key` in config') - - sequence = conf.get('sequence') - if sequence is None: - raise ConfigError('Sequence missing a `sequence` list of actions in config') - return config - - -class Sequence(Component): - """ Automation Sequence - Performs sequence of actions with delays - and conditions inbetween each phase. - """ - - # Current step of the automation (0 index) - _current_step = 0 - - # True if current step delay finished - _delay_complete = False - - # True if current step is completed and delay done - _step_complete = False - - # True if step triggered to advanced - _step_triggered = False - - # Thread safe bool for if sequence is active - _active = threading.Event() - - # Used for duration tracking - _duration_start = time.perf_counter() - - # Tracking delay config vs actual delay duration - _delay_actual = 0 - - # Tracking duration config vs actual step duration - _duration_actual = 0 - - """ Properties """ - @property - def id(self): - """ Unique id or key """ - return self.config.get('key') - - @property - def name(self): - """ Friendly name of control """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def sequence(self): - """ List of actions and delays """ - return self.config.get('sequence', []) - - @property - def current_step(self): - """ Current step from sequence list """ - return self.sequence[self._current_step] - - @property - def step_delay(self): - """ Returns the current step's delay - Delays happen before actions - """ - return self.sequence[self._current_step].get('delay') - - @property - def step_duration(self): - """ Returns the current step's duration - Durations are delays after actions - """ - return self.sequence[self._current_step].get('duration') - - @property - def total_steps(self): - """ List of actions and delays """ - return len(self.sequence) - - @property - def active(self): - """ Thread save active boolean """ - return self._active.is_set() - - @active.setter - def active(self, value): - """ Allows `self.active = False` while still being thread safe """ - if bool(value): - self._active.set() - else: - self._active.clear() - - @property - def duration(self): - """ Return how long the current state has been applied in seconds """ - self._current_duration = time.perf_counter() - self._duration_start - return round(self._current_duration, 4) - - @property - def topic(self): - """ Return the topic to listen on """ - return self.config.get('topic', '').replace(" ", "/").lower() if self.config.get( - 'topic') is not None else f'mudpi/sequences/{self.id}' - - - """ Methods """ - def init(self): - """ Init hook to subscribe to events """ - self.mudpi.events.subscribe(self.topic, self.handle_event) - - def update(self): - """ Main run loop for sequence to check - time past and if it should fire actions """ - if self.mudpi.is_prepared: - try: - if self.active: - if not self._step_complete: - if not self._delay_complete: - if self.step_delay is not None: - if self.duration > self.step_delay: - self._delay_complete = True - self._delay_actual = self.duration - self.reset_duration() - else: - # Waiting break early - return - else: - self._delay_complete = True - self.reset_duration() - - if self._delay_complete: - if not self._step_triggered: - if self.evaluate_thresholds(): - self.trigger() - else: - if self.current_step.get('thresholds') is not None: - # Thresholds failed skip step without trigger - self._step_triggered = True - self._step_complete = True - - if self.step_duration is not None and not self._step_complete: - if self.duration > self.step_duration: - self._step_complete = True - self._duration_actual = self.duration - self.reset_duration() - else: - # Waiting break early - return - else: - # No duration set meaning step only advances - # manualy by calling actions and events. RTM - pass - - if self._step_complete: - self.fire({"event": "SequenceStepEnded"}) - # Logger.log( - # LOG_LEVEL["debug"], - # f'Sequence {FONT_CYAN}{self.name}{FONT_RESET} Step {self._current_step+1} Debug\n' \ - # f'Delay: {self.step_delay} Actual: {self._delay_actual} Duration: {self.step_duration} Actual: {self._duration_actual}' - # ) - return self.next_step() - else: - # Sequence is not active. - self.reset_duration() - except Exception as e: - Logger.log_formatted(LOG_LEVEL["error"], - f'Sequence {self.id}', 'Unexpected Error', 'error') - Logger.log(LOG_LEVEL["critical"], e) - - def fire(self, data={}): - """ Fire a control event """ - event_data = { - 'event': 'SequenceUpdated', - 'component_id': self.id, - 'name': self.name, - 'updated_at': str(datetime.datetime.now().replace(microsecond=0)), - 'state': self.active, - 'step': self._current_step, - 'total_steps': self.total_steps - } - event_data.update(data) - self.mudpi.events.publish(NAMESPACE, event_data) - - def reset_duration(self): - """ Reset the duration of the current state """ - self._duration_start = time.perf_counter() - return True - - def restart(self, event_data=None): - """ Restart the entire sequence from begining """ - self._current_step = 0 - self.reset_step() - self.active = True - self.fire({ - "event": "SequenceRestarted" - }) - Logger.log( - LOG_LEVEL["info"], - f'Sequence {FONT_CYAN}{self.name}{FONT_RESET} Restarted' - ) - - def reset(self, event_data=None): - """ Reset the entire sequence """ - self._current_step = 0 - self.reset_step() - self.fire({ - "event": "SequenceReset" - }) - Logger.log( - LOG_LEVEL["info"], - f'Sequence {FONT_CYAN}{self.name}{FONT_RESET} Reset' - ) - - def reset_step(self, event_data=None): - """ Reset the current step progress """ - self._delay_complete = False - self._step_triggered = False - self._step_complete = False - self.reset_duration() - - def start(self, event_data=None): - """ Start the sequence """ - if not self.active: - self._current_step = 0 - self.active = True - self.reset_step() - self.fire({ - "event": "SequenceStarted" - }) - Logger.log( - LOG_LEVEL["info"], - f'Sequence {FONT_CYAN}{self.name}{FONT_RESET} Started' - ) - - def stop(self, event_data=None): - """ Stop the sequence """ - if self.active: - self._current_step = 0 - self.active = False - self.reset_step() - self.fire({ - "event": "SequenceStopped" - }) - Logger.log( - LOG_LEVEL["info"], - f'Sequence {FONT_CYAN}{self.name}{FONT_RESET} Stopped' - ) - - def next_step(self, event_data=None): - """ Advance to the next sequnce step - Makes sure any delays and durations are done """ - - # Step must be flagged complete to advance - if self._step_complete: - if self.active: - # 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.fire({"event": "SequenceStepStarted"}) - Logger.log_formatted( - LOG_LEVEL["info"], - f'Sequence: {FONT_CYAN}{self.name}{FONT_RESET}', f'Step {self._current_step+1}/{self.total_steps}' - ) - else: - # Last step of sequence completed - self.active = False - self.fire({"event": "SequenceEnded"}) - Logger.log( - LOG_LEVEL["info"], - f'Sequence {FONT_CYAN}{self.name}{FONT_RESET} Completed' - ) - self.reset_duration() - - def previous_step(self, event_data=None): - """ Go to the previous step """ - if self._current_step > 0: - self._current_step -= 1 - self.fire({"event": "SequenceStepStarted" }) - self.reset_step() - self.reset_duration() - - def advance_step(self, event_data=None): - """ Advances the next step non-forcefully - only if delays/duration are completed. - """ - if not self.active: - self.start() - return - - if self.step_duration is None and self._delay_complete: - self._step_complete = True - - self.next_step() - - def skip_step(self, event_data=None): - """ Skips the current step without triggering """ - self._step_complete = True - self._step_triggered = True - - def handle_event(self, message): - """ Process event data for the sequnce """ - data = message['data'] - if data is not None: - _event_data = self.last_event = decode_event_data(data) - try: - if _event_data['event'] == 'SequenceNextStep': - self.advance_step() - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Next Step Triggered' - ) - elif _event_data['event'] == 'SequencePreviousStep': - self.previous_step() - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Previous Step Triggered' - ) - elif _event_data['event'] == 'SequenceStart': - self.start() - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Start Triggered' - ) - elif _event_data['event'] == 'SequenceSkipStep': - self.skip_step() - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Skip Step Triggered' - ) - elif _event_data['event'] == 'SequenceStop': - self.stop() - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Stop Triggered' - ) - except: - Logger.log( - LOG_LEVEL["info"], - f"Error Decoding Event for Sequence {self.id}" - ) - - def trigger(self, value=None): - """ Trigger all the actions for the current step """ - if self._step_triggered: - return - - try: - for action in self.current_step.get('actions', []): - if self.mudpi.actions.exists(action): - _data = value or {} - self.mudpi.actions.call(action, action_data=_data) - except Exception as e: - Logger.log( - LOG_LEVEL["error"], - f"Error triggering sequence action {self.id} ", e) - self._step_triggered = True - return - - def evaluate_thresholds(self): - """ Check critera if step should activate """ - thresholds_passed = False - if self.current_step.get('thresholds') is not None: - for threshold in self.current_step.get('thresholds', []): - key = threshold.get("source") - # Get state object from manager - state = self.mudpi.states.get(key) - if state is not None: - _state = json.loads(state.state) - if threshold.get("nested_source") is not None: - nested_source = threshold['nested_source'].lower() - try: - value = _state.get(nested_source) - except: - value = _state - else: - value = _state - # state = json.loads(state.decode('utf-8')) - 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 - elif comparison == "ex": - if value is not None: - thresholds_passed = True - else: - thresholds_passed = False - else: - # Data was null - # Comparison if data not exists - if comparison == "nex": - thresholds_passed = True - else: - # Threshold set but data not found and - # not checking for 'not exists' - thresholds_passed = False - else: - # No thresholds for this step, proceed. - thresholds_passed = True - return thresholds_passed \ No newline at end of file diff --git a/build/lib/mudpi/extensions/socket/__init__.py b/build/lib/mudpi/extensions/socket/__init__.py deleted file mode 100644 index 4c7d9ad..0000000 --- a/build/lib/mudpi/extensions/socket/__init__.py +++ /dev/null @@ -1,151 +0,0 @@ -""" - Socket Extension - Hosts a socket server for devices - to connect and send data through to - MudPi. -""" -import sys -import json -import time -import socket -import threading -from mudpi.workers import Worker -from mudpi.exceptions import ConfigError -from mudpi.utils import decode_event_data -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.extensions import Component, BaseExtension - - -NAMESPACE = 'socket' - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = 0.05 - - def init(self, config): - """ Preppare the extension components """ - self.config = config #list of lists - self.cache = self.mudpi.cache.setdefault(NAMESPACE, {}) - self.servers = {} - - if not isinstance(config, list): - config = [config] - - for conf in config: - key = conf.get('key') - if key not in self.servers: - self.servers[key] = SocketServer(self.mudpi, conf) - - # self.manager.register_component_actions('shutdown', action='shutdown') - return True - - def validate(self, config): - """ Validate the socket configs """ - _config = config[self.namespace] - if not isinstance(_config, list): - _config = [_config] - - for conf in _config: - key = conf.get('key') - if key is None: - raise ConfigError('Socket server missing a `key` in config') - - host = conf.get('host') - if host is None: - raise ConfigError('Socket server missing a `host` in config') - - port = conf.get('port') - if port is None: - # Defaulting port - conf['port'] = 7007 - # raise ConfigError('Socket server missing a `port` in config') - - return _config - -class SocketServer(Worker): - """ - A socket server used to allow incoming wiresless connections. - MudPi will listen on the socket server for clients to join and - send a message that should be broadcast on the event system. - """ - - @property - def host(self): - return str(self.config.get('host', '127.0.0.1')) - - @property - def port(self): - return int(self.config.get('port', 7007)) - - - def init(self): - """ Setup the socket """ - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.client_threads = [] - self._server_ready = threading.Event() - self._server_ready.set() - self._server_running = False - - try: - self.sock.bind((self.host, self.port)) - except socket.error as msg: - Logger.log(LOG_LEVEL['error'], f'Failed to create socket. Error Code: {str(msg[0])} Error Message: {msg[1]}') - sys.exit() - - def work(self, func=None): - while self.mudpi.is_prepared: - if self.mudpi.is_running: - if not self._server_running: - self._server = threading.Thread(target = self.server, args = ()) - self._server.start() - self._server_running = True - time.sleep(0.1) - self._server_ready.clear() - self.sock.close() - self._server.join() - if len(self.client_threads) > 0: - for client in self.client_threads: - client.join() - Logger.log_formatted(LOG_LEVEL['info'], f'Socket Server {self.key}', 'Offline', 'error') - - def server(self): - """ Socket server main loop """ - self.sock.listen(0) # number of clients to listen for. - Logger.log_formatted(LOG_LEVEL['info'], 'MudPi Server', 'Online', 'success') - while self._server_ready.is_set(): - try: - client, address = self.sock.accept() - client.settimeout(600) - ip, port = client.getpeername() - Logger.log(LOG_LEVEL['info'], f'Socket Client {port} from {ip} Connected') - t = threading.Thread(target = self.listenToClient, args = (client, address, ip)) - self.client_threads.append(t) - t.start() - except Exception as e: - Logger.log(LOG_LEVEL['error'], e) - time.sleep(1) - self.sock.close() - - def listenToClient(self, client, address, ip): - size = 1024 - while self.mudpi.is_prepared: - try: - data = client.recv(size) - if data: - data = decode_event_data(data) - if data.get("topic", None) is not None: - self.mudpi.events.publish(data["topic"], data) - Logger.log(LOG_LEVEL['debug'], f"Socket Event {data['event']} from {data['source']} Dispatched") - # response = { "status": "OK", "code": 200 } - # client.send(json.dumps(response).encode('utf-8')) - else: - Logger.log(LOG_LEVEL['error'], f"Socket Data Recieved. {FONT_RED}Dispatch Failed:{FONT_RESET} Missing Data 'Topic'") - Logger.log(LOG_LEVEL['debug'], data) - else: - pass - # raise error('Client Disconnected') - except Exception as e: - Logger.log(LOG_LEVEL['info'], f"Socket Client {ip} Disconnected") - client.close() - return False - Logger.log_formatted(LOG_LEVEL['info'], 'Closing Client Connection ', 'Complete', 'success') diff --git a/build/lib/mudpi/extensions/state/__init__.py b/build/lib/mudpi/extensions/state/__init__.py deleted file mode 100644 index c6a9402..0000000 --- a/build/lib/mudpi/extensions/state/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" - State Extension - Gives support to interfaces with - the MudPi internal State Manager. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'state' - update_interval = 0.2 - \ No newline at end of file diff --git a/build/lib/mudpi/extensions/state/trigger.py b/build/lib/mudpi/extensions/state/trigger.py deleted file mode 100644 index 8a73765..0000000 --- a/build/lib/mudpi/extensions/state/trigger.py +++ /dev/null @@ -1,88 +0,0 @@ -""" - State Trigger Interface - Monitors state changes and - checks new state against any - thresholds if provided. -""" -from mudpi.utils import decode_event_data -from mudpi.exceptions import ConfigError -from mudpi.extensions import BaseInterface -from mudpi.extensions.trigger import Trigger -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class Interface(BaseInterface): - - def load(self, config): - """ Load state Trigger component from configs """ - trigger = StateTrigger(self.mudpi, config) - if trigger: - self.add_component(trigger) - return True - - def validate(self, config): - """ Validate the trigger config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not config.get('source'): - raise ConfigError('Missing `source` key in Sensor Trigger config.') - - return config - - -class StateTrigger(Trigger): - """ A trigger that listens to states - and checks for new state that - matches any thresholds. - """ - - # Used for onetime subscribe - _listening = False - - - """ Methods """ - def init(self): - """ Listen to the state for changes """ - super().init() - if self.mudpi.is_prepared: - if not self._listening: - # TODO: Eventually get a handler returned to unsub just this listener - self.mudpi.events.subscribe('state', self.handle_event) - self._listening = True - return True - - def handle_event(self, event): - """ Handle the event data from the event system """ - _event_data = decode_event_data(event) - if _event_data.get('event'): - try: - if _event_data['event'] == 'StateUpdated': - if _event_data['component_id'] == self.source: - sensor_value = self._parse_data(_event_data["new_state"]["state"]) - if self.evaluate_thresholds(sensor_value): - self.active = True - if self._previous_state != self.active: - # Trigger is reset, Fire - self.trigger(_event_data) - else: - # Trigger not reset check if its multi fire - if self.frequency == 'many': - self.trigger(_event_data) - else: - self.active = False - except Exception as error: - Logger.log(LOG_LEVEL["error"], - f'Error evaluating thresholds for trigger {self.id}') - Logger.log(LOG_LEVEL["debug"], error) - self._previous_state = self.active - - def unload(self): - # Unsubscribe once bus supports single handler unsubscribes - return - - def _parse_data(self, data): - """ Get nested data if set otherwise return the data """ - return data if self.nested_source is None else data.get(self.nested_source, None) - diff --git a/build/lib/mudpi/extensions/sun/__init__.py b/build/lib/mudpi/extensions/sun/__init__.py deleted file mode 100644 index 3a0b252..0000000 --- a/build/lib/mudpi/extensions/sun/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -""" - Sun Extension - Includes interfaces for getting - sunrise and sunset. -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 'sun' - update_interval = (60 * 60 * 4) # Every 4 hours - - def init(self, config): - """ Prepare the api connection and sun components """ - self.config = config - - return True - - def validate(self, config): - """ Validate the api connection info """ - config = config[self.namespace] - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('latitude'): - raise ConfigError('Missing `latitude` in sun configs.') - - if not conf.get('longitude'): - raise ConfigError('Missing `longitude` in sun configs.') - return config diff --git a/build/lib/mudpi/extensions/sun/sensor.py b/build/lib/mudpi/extensions/sun/sensor.py deleted file mode 100644 index 220c0e8..0000000 --- a/build/lib/mudpi/extensions/sun/sensor.py +++ /dev/null @@ -1,89 +0,0 @@ -""" - Sun Sensor Interface - Connects to sun api to get - sunset and sunrise times. -""" -import time -import json -import requests -from mudpi.utils import decode_event_data -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - # Override the update interval due to event handling - update_interval = 1 - - # Duration tracking - _duration_start = time.perf_counter() - - def load(self, config): - """ Load mqtt sensor component from configs """ - sensor = MQTTSensor(self.mudpi, config) - if sensor: - sensor.connect(self.extension) - self.add_component(sensor) - return True - - def validate(self, config): - """ Validate the mqtt sensor config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('key'): - raise ConfigError('Missing `key` in MQTT sensor config.') - - expires = conf.get('expires') - if not expires: - conf['expires'] = 0 - else: - conf['expires'] = int(conf['expires']) - - return config - - -class SunSensor(Sensor): - """ Sun Sensor - Returns a sunset and sunset from api - """ - - # Track state change - _prev_state = None - - # Connection to mqtt - _conn = None - - # For duration tracking - _duration_start = time.perf_counter() - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - -# url = 'https://api.sunrise-sunset.org/json' - -# response = requests.get(url, -# params={'lat': 42.526, 'lng': -89.043},) - -# if response.status_code == 200: -# print('Success!') -# elif response.status_code == 404: -# print('Not Found.') - -# print(response.json()) \ No newline at end of file diff --git a/build/lib/mudpi/extensions/t9602/__init__.py b/build/lib/mudpi/extensions/t9602/__init__.py deleted file mode 100644 index 26a864f..0000000 --- a/build/lib/mudpi/extensions/t9602/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" - T9602 Extension - Includes sensor interface for T9602. - Works on i2c over linux boards (typically - on a raspberry pi.) -""" -from mudpi.extensions import BaseExtension - - -class Extension(BaseExtension): - namespace = 't9602' - update_interval = 30 - diff --git a/build/lib/mudpi/extensions/t9602/sensor.py b/build/lib/mudpi/extensions/t9602/sensor.py deleted file mode 100644 index 9e8dd7f..0000000 --- a/build/lib/mudpi/extensions/t9602/sensor.py +++ /dev/null @@ -1,101 +0,0 @@ -""" - T9602 Sensor Interface - Connects to a T9602 device to get - environment and climate readings. -""" -import smbus -from mudpi.extensions import BaseInterface -from mudpi.extensions.sensor import Sensor -from mudpi.exceptions import MudPiError, ConfigError - - -class Interface(BaseInterface): - - def load(self, config): - """ Load T9602 sensor component from configs """ - sensor = T9602Sensor(self.mudpi, config) - if sensor.connect(): - self.add_component(sensor) - return True - - def validate(self, config): - """ Validate the T9602 config """ - if not config.get('address'): - # raise ConfigError('Missing `address` in T9602 config.') - config['address'] = 0x28 - else: - addr = config['address'] - - # Convert hex string/int to actual hex - if isinstance(addr, str): - addr = hex(int(addr, 16)) - elif isinstance(addr, int): - addr = hex(addr) - - config['address'] = addr - - return config - - - -class T9602Sensor(Sensor): - """ T9602 Sensor - Get readings for humidity and temperature. - """ - - """ Properties """ - @property - def id(self): - """ Return a unique id for the component """ - return self.config['key'] - - @property - def name(self): - """ Return the display name of the component """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self._state - - @property - def classifier(self): - """ Classification further describing it, effects the data formatting """ - return 'climate' - - - """ Methods """ - def connect(self): - """ Connect to the Device - 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 True - - def update(self): - """ Get data from T9602 device""" - 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: - readings = { - 'temperature': temperature_c, - 'humidity': humidity - } - self._state = readings - return readings - else: - Logger.log( - LOG_LEVEL["error"], - 'Failed to get reading [t9602]. Try again!' - ) - return None diff --git a/build/lib/mudpi/extensions/toggle/__init__.py b/build/lib/mudpi/extensions/toggle/__init__.py deleted file mode 100644 index d551890..0000000 --- a/build/lib/mudpi/extensions/toggle/__init__.py +++ /dev/null @@ -1,146 +0,0 @@ -""" - Toggle Extension - Toggles (formerly 'Relays') are components - that can be toggled i.e. turned on and off. - Control devices like a pump using a toggle. -""" -import time -import datetime -import threading -from mudpi.extensions import Component, BaseExtension - - -NAMESPACE = 'toggle' - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = 0.2 - - def init(self, config): - self.config = config[self.namespace] - - self.manager.init(self.config) - - self.manager.register_component_actions('toggle', action='toggle') - self.manager.register_component_actions('turn_on', action='turn_on') - self.manager.register_component_actions('turn_off', action='turn_off') - return True - - - -class Toggle(Component): - """ Base Toggle - Base Toggle for all toggle interfaces - """ - - # State should be if the toggle is active or not - _state = False - - # Duration tracking - _duration_start = time.perf_counter() - - # Thread safe bool for if sequence is active - _active = threading.Event() - - - """ Properties """ - @property - def id(self): - """ Unique id or key """ - return self.config.get('key') - - @property - def name(self): - """ Friendly name of toggle """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def state(self): - """ Return the state of the component (from memory, no IO!) """ - return self.active - - @property - def invert_state(self): - """ Set to True to make OFF state fire events instead of ON state """ - return self.config.get('invert_state', False) - - @property - def max_duration(self): - """ Max duration in seconds the toggle can remain active """ - return self.config.get('max_duration') - - @property - def duration(self): - """ Return how long the current state has been applied in seconds """ - self._current_duration = time.perf_counter() - self._duration_start - return round(self._current_duration, 4) - - @property - def active(self): - """ Thread save active boolean """ - return self._active.is_set() - - @active.setter - def active(self, value): - """ Allows `self.active = False` while still being thread safe """ - if bool(value): - self._active.set() - else: - self._active.clear() - - - """ Methods """ - def update(self): - """ Update doesn't actually update the state - but is instead used for failsafe checks and - event detection. """ - if self.max_duration: - if self.active: - if self.duration > self.max_duration: - # Failsafe cutoff duration - self.turn_off() - - def restore_state(self, state={}): - """ This is called on start to - restore previous state """ - self._state = state.get('state', False) - return - - def fire(self, data={}): - """ Fire a toggle event """ - event_data = { - 'event': 'ToggleUpdate', - 'component_id': self.id, - 'name': self.name, - 'updated_at': str(datetime.datetime.now().replace(microsecond=0)), - 'state': self.state, - 'invert_state': self.invert_state - } - event_data.update(data) - self.mudpi.events.publish(NAMESPACE, event_data) - - def reset_duration(self): - """ Reset the duration of the toggles current state """ - self._duration_start = time.perf_counter() - return True - - def unload(self): - """ Called during shutdown for cleanup operations """ - self.turn_off() - - - """ Actions """ - def toggle(self, data={}): - """ Toggle the device """ - self.active = not self.active - return self.active - - def turn_on(self, data={}): - """ Turn on the device """ - self.active = True - return self.active - - def turn_off(self, data={}): - """ Turn off the device """ - self.active = False - return self.active \ No newline at end of file diff --git a/build/lib/mudpi/extensions/toggle/trigger.py b/build/lib/mudpi/extensions/toggle/trigger.py deleted file mode 100644 index 88e2d19..0000000 --- a/build/lib/mudpi/extensions/toggle/trigger.py +++ /dev/null @@ -1,88 +0,0 @@ -""" - Toggle Trigger Interface - Monitors control state changes and - checks new state against any - thresholds if provided. -""" -from mudpi.utils import decode_event_data -from mudpi.exceptions import ConfigError -from mudpi.extensions import BaseInterface -from mudpi.extensions.trigger import Trigger -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class Interface(BaseInterface): - - def load(self, config): - """ Load toggle Trigger component from configs """ - trigger = ToggleTrigger(self.mudpi, config) - if trigger: - self.add_component(trigger) - return True - - def validate(self, config): - """ Validate the trigger config """ - if not isinstance(config, list): - config = [config] - - for conf in config: - if not conf.get('source'): - raise ConfigError('Missing `source` key in Sensor Trigger config.') - - return config - - -class ToggleTrigger(Trigger): - """ A trigger that listens to states - and checks for new state that - matches any thresholds. - """ - - # Used for onetime subscribe - _listening = False - - - """ Methods """ - def init(self): - """ Listen to the state for changes """ - super().init() - if self.mudpi.is_prepared: - if not self._listening: - # TODO: Eventually get a handler returned to unsub just this listener - self.mudpi.events.subscribe('toggle', self.handle_event) - self._listening = True - return True - - def handle_event(self, event): - """ Handle the event data from the event system """ - _event_data = decode_event_data(event) - if _event_data.get('event'): - try: - if _event_data['event'] == 'ToggleUpdated': - if _event_data['component_id'] == self.source: - sensor_value = self._parse_data(_event_data["state"]) - if self.evaluate_thresholds(sensor_value): - self.active = True - if self._previous_state != self.active: - # Trigger is reset, Fire - self.trigger(_event_data) - else: - # Trigger not reset check if its multi fire - if self.frequency == 'many': - self.trigger(_event_data) - else: - self.active = False - except Exception as error: - Logger.log(LOG_LEVEL["error"], - f'Error evaluating thresholds for trigger {self.id}') - Logger.log(LOG_LEVEL["debug"], error) - self._previous_state = self.active - - def unload(self): - # Unsubscribe once bus supports single handler unsubscribes - return - - def _parse_data(self, data): - """ Get nested data if set otherwise return the data """ - return data if self.nested_source is None else data.get(self.nested_source, None) - diff --git a/build/lib/mudpi/extensions/trigger/__init__.py b/build/lib/mudpi/extensions/trigger/__init__.py deleted file mode 100644 index 7bcf0a1..0000000 --- a/build/lib/mudpi/extensions/trigger/__init__.py +++ /dev/null @@ -1,227 +0,0 @@ -""" - Trigger Extension - Causes actions based on situations created - by events or state changed. Thresholds can - be set to define more specific paramerters. -""" -import datetime -import threading -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.extensions import Component, BaseExtension - - -NAMESPACE = 'trigger' - -class Extension(BaseExtension): - namespace = NAMESPACE - update_interval = 0.05 - - def init(self, config): - self.config = config[self.namespace] #list of lists - self.cache = self.mudpi.cache.setdefault(NAMESPACE, {}) - trigger_cache = self.cache.setdefault('triggers', {}) - - # Manually load interfaces in order to load group triggers - # after all the other triggers because groups depend on - # all other triggers to be loaded first. - _triggers, _groups = split_trigger_configs(self.config) - self.manager.init(self.config, load_interfaces=False) - if _triggers: - self.manager.load_interfaces(_triggers) - if _groups: - self.manager.load_interfaces(_groups) - - self.manager.register_component_actions('trigger', action='trigger') - return True - - -class Trigger(Component): - """ Base Trigger - Base class for all trigger interfaces - """ - _actions = [] - - # Used for trigger groups - group = None - - # Thread safe active boolean - _active = threading.Event() - - # Used to fire triggers `once` or `many` - _previous_state = False - - """ Properties """ - @property - def id(self): - """ Unique id or key """ - return self.config.get('key') - - @property - def name(self): - """ Friendly name of toggle """ - return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" - - @property - def source(self): - """ Source key to get event data """ - return self.config.get('source', '').lower() - - @property - def nested_source(self): - """ If source is a dict, this is a key in it to get data """ - return self.config.get('nested_source', '').lower() - - @property - def frequency(self): - """ Set if should fire continuously while active - Options: `once` or `many` - """ - return self.config.get('frequency', 'once') - - @property - def thresholds(self): - """ List of thresholds to check data against """ - return self.config.get('thresholds', []) - - @property - def actions(self): - """ Keys of actions to call if triggered """ - return self.config.get('actions', []) - - @property - def active(self): - """ Thread save active boolean """ - return self._active.is_set() - - @active.setter - def active(self, value): - """ Allows `self.active = False` while still being thread safe """ - if bool(value): - self._active.set() - else: - self._active.clear() - - - """ Methods """ - def init(self): - """ Register Trigger to cache after __init__ """ - self.cache = self.mudpi.cache.setdefault(NAMESPACE, {}) - trigger_cache = self.cache.setdefault('triggers', {}) - trigger_cache[self.id] = self - - def check(self): - """ Main trigger check loop to determine if - trigger should fire. """ - return - - def update(self): - """ Update doesn't actually update the state - but is instead used for listening to events, - tracking state / time change and triggering. """ - if self.mudpi.is_prepared: - self.check() - - def evaluate_thresholds(self, value): - """ Check if conditions are met to fire trigger """ - 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 - elif comparison == "ex": - if value is not None: - thresholds_passed = True - else: - thresholds_passed = False - - return thresholds_passed - - def fire(self, data={}): - """ Fire an event """ - event_data = { - 'event': 'TriggerFired', - 'id': self.id, - 'name': self.name, - 'updated_at': str(datetime.datetime.now().replace(microsecond=0)), - 'state': self.state, - 'source': self.source, - } - event_data.update(data) - self.mudpi.events.publish(NAMESPACE, event_data) - - def trigger(self, value=None): - """ Fire off any actions or sequences """ - try: - self.fire({'trigger_value': value}) - # Trigger the actions of the trigger - for action in self.actions: - if self.mudpi.actions.exists(action): - _data = value or {} - self.mudpi.actions.call(action, action_data=_data) - - except Exception as e: - Logger.log(LOG_LEVEL["error"], - f"Error triggering action {self.id}. \n{e}") - return - - def unload(self): - """ Called during shutdown for cleanup operations """ - pass - - -""" Helper """ -def split_trigger_configs(config): - """ Seperate out group triggers from configs """ - _triggers = [] - _groups = [] - for conf in config: - if not conf: - continue - if not isinstance(conf, list): - conf = [conf] - - for entry in conf: - try: - # Search for interface property - interface = entry.get("interface") - except AttributeError as error: - interface = None - - if interface is None: - continue - - if interface == 'group': - _groups.append(conf) - else: - _triggers.append(conf) - - return _triggers, _groups diff --git a/build/lib/mudpi/importer.py b/build/lib/mudpi/importer.py deleted file mode 100644 index cff7417..0000000 --- a/build/lib/mudpi/importer.py +++ /dev/null @@ -1,757 +0,0 @@ -""" MudPi Importer - - Loads built in extensions and custom extensions dynamically - to the system. Data is cached to avoid multiple imports of the - same extension. Extensions will be loaded at runtime based on - configs with each root config key being the extension namespace. -""" -import os -import sys -import json -import pkgutil -import importlib -from mudpi import utils -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.exceptions import MudPiError, ConfigError, ExtensionNotFound, RecursiveDependency -from mudpi.constants import __version__, PATH_CONFIG, DEFAULT_CONFIG_FILE, FONT_RESET_CURSOR, FONT_RESET, YELLOW_BACK, GREEN_BACK, \ - FONT_GREEN, FONT_RED, FONT_YELLOW, RED_BACK, FONT_PADDING - - -def available_extensions(mudpi, extensions_pkg): - """ Gets a dict of available extensions in a namespace package """ - cache = mudpi.cache.get("discovered_extensions") - - if cache is not None: - return cache - - cache = mudpi.cache["discovered_extensions"] = { name: ispkg - for importer, name, ispkg in - pkgutil.iter_modules(extensions_pkg.__path__, f'{extensions_pkg.__name__}.') } - return cache - - -def get_extension_importer(mudpi, extension, install_requirements=False): - """ Find or create an extension importer, Loads it if not loaded, - Checks cache first. - - Set install_requirements to True to also have all requirements - checked through pip. - """ - - # First we check if the namespace is disabled. Could be due to errors or configs - disabled_cache = mudpi.cache.setdefault("disabled_namespaces", {}) - if extension in disabled_cache: - raise MudPiError(f"Extension is {extension} is disabled.") - - if install_requirements: - extension_importer = _extension_with_requirements_installed(mudpi, extension) - if extension_importer is not None: - return extension_importer - - importer_cache = mudpi.cache.setdefault("extension_importers", {}) - - try: - extension_importer = importer_cache.get(extension) - if extension_importer is not None: - return extension_importer - except Exception as error: - extension_importer = None - - if extension_importer is None: - extension_importer = _get_custom_extensions(mudpi).get(extension) - if extension_importer is not None: - Logger.log( - LOG_LEVEL["warning"], - f'{FONT_YELLOW}You are using {extension} which is not provided by MudPi.{FONT_RESET}\nIf you experience errors, remove it.' - ) - return extension_importer - - # Component not found look in internal extensions - from mudpi import extensions - - extension_importer = ExtensionImporter.create(mudpi, extension, extensions) - - if extension_importer is not None: - importer_cache[extension] = extension_importer - Logger.log_formatted( - LOG_LEVEL["info"], f'{extension_importer.namespace.title()} Ready for Import', - 'Ready', 'success' - ) - else: - Logger.log_formatted( - "error", f'Import Preperations for {extension.title()}', - 'Failed', 'error' - ) - Logger.log( - LOG_LEVEL["debug"], - f'{FONT_YELLOW}`{extension.title()}` was not found.{FONT_RESET}' - ) - disabled_cache[extension] = 'Not Found' - raise ExtensionNotFound(extension) - - return extension_importer - - -class ExtensionImporter: - """ This class prepares and loads extensions. - An extension is dynamically loaded into MudPi - and contains interfaces with components for the system. - """ - - @classmethod - def create(cls, mudpi, extension_name, extensions_module): - """ Static method to load extension """ - for path in extensions_module.__path__: - config_path = os.path.join(path, extension_name, "extension.json") - - if not os.path.isfile(config_path): - continue - - try: - with open(config_path) as f: - config = json.loads(f.read()) - except FileNotFoundError: - Logger.log( - LOG_LEVEL["error"], - f'{FONT_RED}No extension.json found at {config_path}.{FONT_RESET}' - ) - continue - except Exception as e: - Logger.log( - LOG_LEVEL["error"], - f'{FONT_RED}Error loading extension.json at {config_path} {error}.{FONT_RESET}' - ) - continue - - return cls(mudpi, config, f"{extensions_module.__name__}.{extension_name}", os.path.split(config_path)[0]) - - return None - - - def __init__(self, mudpi, config, extension_path, file_path): - self.mudpi = mudpi - self.config = config - self.extension_path = extension_path - self.file_path = file_path - - self.extension = None - self.module = None - self.interfaces = {} - - if self.has_dependencies: - self.loaded_dependencies = None - self.dependencies_ready = None - else: - self.loaded_dependencies = {} - self.dependencies_ready = True - - if self.has_requirements: - self.requirements_installed = None - else: - self.requirements_installed = True - - """ Properties """ - @property - def name(self): - """ Returns the extension human readable display name """ - return self.config.get('name') - - @property - def namespace(self): - """ Returns the extension namespace (a-z snakecase) - Must be unique and the same as the extension folder name - i.e. rpi_gpio - """ - return self.config.get('namespace') - - @property - def details(self): - """ Returns the extension details dict """ - return self.config.setdefault('details', {}) - - @property - def description(self): - """ Returns the extension description from the details """ - return self.config.get('details', {}).get('description') - - @property - def documentation(self): - """ Returns the extension documentation from the details """ - return self.config.get('details', {}).get('documentation') - - @property - def has_requirements(self): - """ Returns if there are requirements in the extension.json """ - return bool(self.requirements) - - @property - def requirements(self): - """ Returns any requirements in the extension.json (list) """ - return self.config.get('requirements') - - @property - def has_dependencies(self): - """ Returns if there are dependencies in the extension.json """ - return bool(self.dependencies) - - @property - def dependencies(self): - """ Returns any dependencies in the extension.json """ - return self.config.get('dependencies') - - """ Methods """ - def prepare_for_import(self): - """ - Loads all the extensions dependencies and installs - all the requirements before we import the extension - """ - - cache = self.mudpi.cache.setdefault('extensions_import_ready', {}) - if cache.get(self.namespace) is not None: - # Already processed - return cache[self.namespace] - - if self.has_dependencies: - if not self.import_dependencies(): - # The dependencies were not able to load - return False - - if self.has_requirements: - if not self.install_requirements(): - # The requirements were not able to install - return False - - cache[self.namespace] = self - return cache[self.namespace] - - - def prepare_interface_and_import(self, interface_name): - """ - Loads all the interface dependencies and installs - all the requirements before we import the interface. - """ - - disabled_cache = self.mudpi.cache.setdefault("disabled_namespaces", {}) - if interface_name in disabled_cache: - raise MudPiError(f"Interface is in disabled namespace: {interface_name}.") - - interface_path = f'{self.namespace}.{interface_name}' - - # Check if interface is already imported - if self.mudpi.extensions.exists(interface_path): - return self.mudpi.extensions.get(interface_path) - - try: - interface_importer = get_extension_importer(self.mudpi, interface_name) - interface_importer.prepare_for_import() - except ExtensionNotFound as error: - return False - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - f'Extension {FONT_YELLOW}{interface_name}{FONT_RESET} had error while preparing to import. {error}' - ) - return False - - try: - interface = interface_importer.get_or_load_interface(self.namespace) - except ImportError as error: - Logger.log( - LOG_LEVEL["error"], - f'Extension {interface_name} has no Interface {FONT_YELLOW}{self.namespace}{FONT_RESET}.' - ) - return False - - if not self.mudpi.extensions.exists(interface_importer.namespace): - # Extension for interface not setup yet and needs to call `init()` - try: - # Import and init() extension for interface - extension = interface_importer.import_extension(self.mudpi.config.config) - except ImportError as error: - Logger.log( - LOG_LEVEL["error"], - f'Extension {interface_name} was unable to be imported.' - ) - return False - if not extension: - Logger.log( - LOG_LEVEL["error"], - f'Problem importing extension {interface_name} for interface.' - ) - return False - else: - extension = self.mudpi.extensions.get(interface_name) - - return interface, extension - - - def import_extension(self, config): - """ Prepare and import the actual extension module """ - disabled_cache = self.mudpi.cache.setdefault("disabled_namespaces", {}) - - if self.mudpi.extensions.exists(self.namespace): - # Extension already loaded - self.extension = self.mudpi.extensions.get(self.namespace) - return self.extension - - # Load all Dependencies and install requirements - if not self.prepare_for_import(): - # The requirements were not able to install - return False - - # Import the actual package now that requirements and dependencies are done - try: - self.extension = self.get_or_load_extension() - # Call extension post import hook - self.extension.extension_imported(importer=self) - except ImportError as error: - Logger.log( - LOG_LEVEL["error"], - f'Error during import of extension: {FONT_YELLOW}{error}{FONT_RESET}' - ) - return False - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - f'Extension {self.namespace} error during import: {FONT_YELLOW}{error}{FONT_RESET}' - ) - return False - - ### Config ### - # Now we can deal with the config - validated_config = self.validate_config(config) - - if not validated_config: - Logger.log( - LOG_LEVEL["error"], - f'Extension {FONT_YELLOW}{self.namespace}{FONT_RESET} has invalid or empty configs.' - ) - return False - - ### Init ### - # Call the extension init with the validated configs - if self.extension.__class__.init == self.extension.__class__.__bases__[0].init: - Logger.log( - LOG_LEVEL["debug"], f"Extension {self.namespace} did not define an `init()` method." - ) - - Logger.log_formatted( - LOG_LEVEL["debug"], f"Initializing {self.namespace.title()}", "Pending", 'notice' - ) - init_result = self.extension.init(validated_config) - - - if init_result: - Logger.log_formatted( - LOG_LEVEL["warning"], f"Initialized {self.namespace.title()}", "Success", 'success' - ) - # Call extension post init hook - self.extension.extension_initialized(importer=self, validated_config=validated_config) - else: - Logger.log_formatted( - LOG_LEVEL["error"], f"{self.namespace.title()} `init()` failed to return True", 'Failed', 'error' - ) - return False - - self.mudpi.extensions.register(self.namespace, self.extension) - self.mudpi.events.publish('core', {'event': 'ExtensionRegistered', 'namespace': self.namespace}) - # Call extension post registry hook - self.extension.extension_registered(importer=self, validated_config=validated_config) - return self.extension - - - def validate_config(self, config): - """ Validate configs for an extension - Returns False or the validated configs - """ - - if not self.namespace: - Logger.log( - LOG_LEVEL["error"], - f'{FONT_YELLOW}{self.namespace.title()}{FONT_RESET} is missing a namespace in `extension.json`' - ) - return False - - namespace = self.namespace - - if self.extension is None: - Logger.log( - LOG_LEVEL["error"], - f'{FONT_YELLOW}{namespace.title()}{FONT_RESET} is not imported. Call `import_extension()` first!' - ) - return False - - - if config is None: - Logger.log( - LOG_LEVEL["error"], - f'{FONT_YELLOW}{namespace.title()}{FONT_RESET} config is None and unable to validate.' - ) - return False - - - # Check for overrided validate() function to determine if custom validation is set - validated_config = config - if self.module.Extension.validate != self.module.Extension.__bases__[0].validate: - # Logger.log_formatted( - # LOG_LEVEL["debug"], - # f'Checking Extension {namespace} Configuration', - # "Validating", 'notice' - # ) - try: - validated_config = self.extension.validate(config) - Logger.log_formatted( - LOG_LEVEL["debug"], - f'{namespace.title()} Configuration', - "Validated", 'success' - ) - except (ConfigError, MudPiError) as error: - Logger.log_formatted( - LOG_LEVEL["error"], - f'{namespace.title()} Configuration Validation', - "Failed", 'error' - ) - Logger.log( - LOG_LEVEL["error"], - f'{namespace.title()} validation error: {error}' - ) - return False - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - f'{namespace.title()} Validator encountered unknown error. \n{error}' - ) - return False - - return validated_config - - # Custom validator not set proceed with default validation - # Todo: Change this to a regex match to prevent false matches - # (i.e. action matches action_other) - conf_keys = [ key - for key in config.keys() - if namespace in key ] - # Logger.log_formatted( - # LOG_LEVEL["debug"], - # f'Checking Extension {namespace} Configuration', - # "Validating", 'notice' - # ) - - interfaces = [] - - disabled_cache = self.mudpi.cache.setdefault("disabled_namespaces", {}) - - for conf_key in conf_keys: - for interface_config in config[conf_key]: - - # Empty configs found - if not interface_config: - continue - - # List wrapper - if not isinstance(interface_config, list): - interface_config = [interface_config] - - for entry in interface_config: - try: - # Search for interface property - interface_name = entry.get("interface") - except AttributeError as error: - interface_name = None - - # No interface to load which is ok. Not all extensions - # support interfaces. i.e. actions - if interface_name is None: - interfaces.append(interface_config) - continue - - if interface_name in disabled_cache: - # raise MudPiError(f"Interface is in disabled namespace: {interface}.") - Logger.log_formatted( - LOG_LEVEL["warning"], - f'Interface {interface_name} is in disabled namespace', - "Disabled", 'error' - ) - continue - # Interface found, attempt to load in order to validate config - try: - interface_importer = get_extension_importer(self.mudpi, interface_name, install_requirements=True) - except Exception as error: - continue - - interface = None - try: - interface_module = interface_importer.get_or_load_interface(namespace) - if not hasattr(interface_module, 'Interface'): - raise MudPiError(f'No `Interface()` class to load for {namespace}:{interface_name}.') - if utils.is_interface(interface_module.Interface): - interface = self.interfaces[interface_name] = interface_module.Interface(self.mudpi, namespace, interface_name) - else: - raise MudPiError(f'Interface {namespace}:{interface} does not extend BaseInterface.') - except MudPiError as error: - Logger.log(LOG_LEVEL["error"], error) - continue - except Exception as error: - Logger.log_formatted( - LOG_LEVEL["error"], - f'Interface {interface_name}:{namespace} error during validation ', - "Errors", 'error' - ) - Logger.log(LOG_LEVEL["debug"], error) - disabled_cache[interface_name] = error - continue - - validated_interface_config = interface_config - - if interface: - try: - validated_interface_config = interface.validate(interface_config) - Logger.log_formatted( - LOG_LEVEL["debug"], - f'Configuration for {interface_importer.namespace}:{namespace}', - "Validated", 'success' - ) - except (ConfigError, MudPiError) as error: - Logger.log_formatted( - LOG_LEVEL["error"], - f'{interface_importer.namespace}:{namespace} Configuration Validation', - "Failed", 'error' - ) - Logger.log( - LOG_LEVEL["error"], - f'{namespace.title()} validation error: {error}' - ) - continue - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - f'{interface_importer.namespace.title()} `validate()` encountered unknown error. \n{error}' - ) - continue - - interfaces.append(validated_interface_config) - Logger.log_formatted( - LOG_LEVEL["debug"], - f'{namespace.title()} Configuration', - "Validated", 'success' - ) - # Copy old config and replace old data with validated - validated_config = validated_config.copy() - for key in conf_keys: - del validated_config[key] - validated_config[namespace] = interfaces - config = validated_config - return validated_config - - - def get_or_load_extension(self): - """ Get the extension base module, import if not """ - module_cache = self.mudpi.cache.setdefault('extension_modules', {}) - if self.namespace not in module_cache: - Logger.log_formatted( - LOG_LEVEL["debug"], f'Importing {self.namespace.title()}', - 'Pending', 'notice' - ) - module_cache[self.namespace] = importlib.import_module(self.extension_path) - Logger.log_formatted( - LOG_LEVEL["info"], f'Imported {self.namespace.title()}', - 'Success', 'success' - ) - self.module = module_cache[self.namespace] - - extension_cache = self.mudpi.cache.setdefault("extensions", {}) - if self.namespace not in extension_cache: - if not hasattr(self.module, 'Extension'): - raise ExtensionNotFound(self.namespace) - - extension = extension_cache[self.namespace] = self.module.Extension(self.mudpi) - - return extension_cache[self.namespace] - - - def get_or_load_interface(self, interface_name): - """ Get a interface from the extension, import it if not in cache""" - cache = self.mudpi.cache.setdefault("interface_modules", {}) - component_fullname = f'{self.namespace}.{interface_name}' - if component_fullname not in cache: - Logger.log_formatted( - LOG_LEVEL["debug"], f'Importing {self.namespace}:{interface_name}', - 'Pending', 'notice' - ) - cache[component_fullname] = \ - importlib.import_module(f'{self.extension_path}.{interface_name}') - Logger.log_formatted( - LOG_LEVEL["info"], f'Imported {self.namespace}:{interface_name}', - 'Success', 'success' - ) - return cache[component_fullname] - - def import_dependencies(self): - """ Import the other extensions this one depends on first to avoid errors """ - if self.dependencies_ready is not None: - return self.dependencies_ready - - try: - dependencies = _load_extension_dependencies(self.mudpi, self) - dependencies.remove(self.namespace) - self.loaded_dependencies = dependencies - self.dependencies_ready = True - except ExtensionNotFound as error: - Logger.log( - LOG_LEVEL["error"], - f'{FONT_RED}Extension dependency not found. {error}.{FONT_RESET}' - ) - self.dependencies_ready = False - except RecursiveDependency as error: - Logger.log( - LOG_LEVEL["error"], - f'{FONT_RED}Extension {self.namespace} recursive dependency. {error}.{FONT_RESET}' - ) - self.dependencies_ready = False - - return self.dependencies_ready - - def install_requirements(self): - """ Check for requirements and make sure they are installed """ - if self.requirements_installed is not None: - return self.requirements_installed - - requirements_installed = _install_extension_requirements(self.mudpi, self) - self.requirements_installed = bool(requirements_installed) - return self.requirements_installed - - def __repr__(self): - """ Debug display of importer. """ - return f'' - - -""" Internal Methods """ -def _get_custom_extensions(mudpi): - """ Returns list of custom extensions in MudPi, checks cache first """ - if mudpi is None: - return {} - - extension_list = mudpi.cache.setdefault('custom_extension_importers', {}) - - if extension_list: - return extension_list - - try: - import custom_extensions - except ImportError: - return {} - - extension_dirs = [ extension_dir - for path in custom_extensions.__path__ - for extension_dir in os.listdir(path) - if os.path.isdir(os.path.join(path, extension_dir)) ] - - extension_list = {} - - for extension in extension_dirs: - extension_importer = ExtensionImporter.create(mudpi, extension, custom_extensions) - if extension_importer is not None: - extension_list[extension_importer.name]: extension_importer - - mudpi.cache['custom_extension_importers'] = extension_list - - return extension_list - - -def _extension_with_requirements_installed(mudpi, extension): - """ Fetch an extension with all the requirements installed - including any requirements defined on dependencies. """ - - cache = mudpi.cache.setdefault('extensions_requirements_installed', {}) - - if cache.get(extension) is not None: - # Already processed - return cache[extension] - - extension_importer = get_extension_importer(mudpi, extension) - - if not extension_importer.install_requirements(): - # The requirements were not able to install - return False - - cache[extension] = extension_importer - return extension_importer - - -def _load_extension_dependencies(mudpi, extension, loading_extensions = [], loaded_extensions = []): - """ Loads extension dependencies recursively """ - namespace = extension.namespace - loading_extensions.append(namespace) - - for dependency in extension.dependencies: - # Check if dependency is already loaded - if dependency in loaded_extensions: - continue - - # Check if already loading, in which we have reference loop - if dependency in loading_extensions: - raise RecursiveDependency(extension, dependency) - - dependency_extension = get_extension_importer(mudpi, dependency) - - if dependency_extension: - loaded_extensions.append(dependency) - - if dependency_extension.has_dependencies: - # Dependency inception level 3, any deeper we hit limbo... - sub_dependencies = _load_extension_dependencies(mudpi, - dependency_extension, loading_extensions, loaded_extensions) - - loaded_extensions.extend(sub_dependencies) - - loaded_extensions.append(namespace) - loading_extensions.remove(namespace) - - return loaded_extensions - - -def _install_extension_requirements(mudpi, extension): - """ Installs all the extension requirements """ - cache = mudpi.cache.setdefault('extensions_requirements_installed', {}) - - if cache.get(extension.namespace) is not None: - # Already processed and installed - return cache[extension.namespace] - - # Handle all the dependencies requirements - if extension.has_dependencies: - if extension.import_dependencies(): - for dependency in extension.loaded_dependencies: - try: - dependency_extension = get_extension_importer(mudpi, dependency) - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - f'Error getting extension <{extension}> dependency: {FONT_YELLOW}{dependency}{FONT_RESET}' - ) - if not dependency_extension.install_requirements(): - Logger.log( - LOG_LEVEL["error"], - f'Error with extension <{extension}> dependency: {FONT_YELLOW}{dependency}{FONT_RESET} requirements.' - ) - - if not extension.has_requirements: - cache[extension.namespace] = extension - return cache[extension.namespace] - - for requirement in extension.requirements: - if not utils.is_package_installed(requirement): - Logger.log_formatted( - LOG_LEVEL["info"], - f'{FONT_YELLOW}{extension.namespace.title()}{FONT_RESET} requirements', - 'Installing', 'notice' - ) - if not utils.install_package(requirement): - Logger.log( - LOG_LEVEL["error"], - f'Error installing <{extension.title()}> requirement: {FONT_YELLOW}{requirement}{FONT_RESET}' - ) - return False - # extension.requirements_installed = True - cache[extension.namespace] = extension - return cache[extension.namespace] \ No newline at end of file diff --git a/build/lib/mudpi/logger/Logger.py b/build/lib/mudpi/logger/Logger.py deleted file mode 100644 index d50ab9b..0000000 --- a/build/lib/mudpi/logger/Logger.py +++ /dev/null @@ -1,167 +0,0 @@ -import sys -import logging - -from mudpi.constants import FONT_RED, FONT_YELLOW, FONT_GREEN, FONT_RESET_CURSOR, \ - FONT_RESET, FONT_PADDING, FONT_RESET, YELLOW_BACK, RED_BACK, GREEN_BACK - -LOG_LEVEL = { - "unknown": logging.NOTSET, - "debug": logging.DEBUG, - "info": logging.INFO, - "warning": logging.WARN, - "error": logging.ERROR, - "critical": logging.CRITICAL, -} - - -class Logger: - logger = None - - def __init__(self, config: dict): - if "logging" in config.keys(): - if config["logging"]: - logger_config = config["logging"] - else: - logger_config = { - "file_log_level": "warning", - "file": "mudpi.log", - "terminal_log_level": "info" - } - else: - logger_config = { - "file_log_level": "warning", - "file": "mudpi.log", - "terminal_log_level": "info" - } - # raise Exception("No Logger configs were found!") - - self.__log = logging.getLogger(config.get('mudpi').get('name', 'mudpi') + "_stream") - self.__file_log = logging.getLogger(config.get('mudpi').get('name', 'mudpi')) - - try: - file_log_level = LOG_LEVEL[logger_config["file_log_level"]] if not \ - config["mudpi"]["debug"] else LOG_LEVEL["debug"] - stream_log_level = LOG_LEVEL[ - logger_config["terminal_log_level"]] if not config["mudpi"][ - "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 - file_handler = logging.FileHandler(logger_config['file']) - file_handler.setLevel(file_log_level) - - self.WRITE_TO_FILE = True - else: - 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)) - - 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") - stream_formatter = logging.Formatter("%(message)s") - file_handler.setFormatter(file_formatter) - stream_handler.setFormatter(stream_formatter) - - 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): - """ - Logs the given message ONLY to the log file. - """ - if Logger.log is not None: - Logger.logger.log_this_file(log_level, msg) - - @staticmethod - def log(log_level, msg: str): # for ease of access from outside - """ - Logs the given message to the terminal and possibly file. - """ - if Logger.log is not None: - try: - log_level = int(log_level) - except ValueError: - if log_level in LOG_LEVEL: - log_level = LOG_LEVEL[log_level] - else: - log_level = LOG_LEVEL['unknown'] - Logger.logger.log_this(log_level, msg) - - @staticmethod - def log_formatted(log_level, message, status='', status_level=None, padding=FONT_PADDING, spacer="."): - """ Log a formatted message with a status level """ - status_color = '' - if status_level == 'success': - status_color = FONT_GREEN - elif status_level == 'warning' or status_level == 'notice': - status_color = FONT_YELLOW - elif status_level == 'error' or status_level == 'critical': - status_color = FONT_RED - - # In order to account for hidden characters we manually format message - # to allow fstrings to be passed in without breaking the format - filter_strings = [ FONT_RED, FONT_GREEN, FONT_YELLOW, FONT_RESET, - RED_BACK, GREEN_BACK, YELLOW_BACK, FONT_PADDING, FONT_RESET_CURSOR ] - - msg_copy = message - hidden_char_len = 0 - - for filter_item in filter_strings: - while str(filter_item) in msg_copy: - hidden_char_len += len(filter_item) - msg_copy = msg_copy.replace(filter_item, "") - - adjusted_padding = padding - len(message) + hidden_char_len - msg = message + ' ' - for i in range(adjusted_padding): - msg += spacer - msg += ' ' + status_color + status + FONT_RESET - - if Logger.log is not None: - return Logger.logger.log(log_level, msg) - - - - - def log_this_file(self, log_level, msg): - msg = str(msg) - - filter_strings = [ FONT_RED, FONT_GREEN, FONT_YELLOW, FONT_RESET, - RED_BACK, GREEN_BACK, YELLOW_BACK, FONT_PADDING, FONT_RESET_CURSOR ] - - for filter_item in filter_strings: - while str(filter_item) in msg: - msg = msg.replace(str(filter_item), "") - - msg = msg.replace("\x1b", "") - msg = msg.replace("\033", "") - msg = msg.replace("[0;0m", "") - msg = msg.replace("[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: - self.log_this_file(log_level, msg) diff --git a/build/lib/mudpi/logger/__init__.py b/build/lib/mudpi/logger/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/managers/__init__.py b/build/lib/mudpi/managers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/managers/core_manager.py b/build/lib/mudpi/managers/core_manager.py deleted file mode 100644 index 268046d..0000000 --- a/build/lib/mudpi/managers/core_manager.py +++ /dev/null @@ -1,261 +0,0 @@ -""" -MudPi Core Manager - -Handles any functions used to prepare resources for -the MudPi system before booting. The manager will -need a MudPi core instance to perform operations. -""" - -import os -import sys -import time -import redis -from mudpi import importer, utils, core -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.exceptions import ExtensionNotFound, RecursiveDependency, ConfigError, MudPiError, ConfigNotFoundError -from mudpi.constants import FONT_RESET, FONT_GREEN, FONT_RED, FONT_YELLOW, RED_BACK, YELLOW_BACK, FONT_PADDING - -class CoreManager: - """ Core Manager Class """ - - def __init__(self, mudpi=None): - self.mudpi = mudpi or core.MudPi() - - def load_mudpi_from_config(self, config_path): - """ Load the MudPi Core from a validated config """ - self.validate_config(config_path) - self.config_path = config_path - # Config valid, attempt to parse in the data - self.mudpi.load_config(config_path=config_path) - - return self.mudpi - - def load_mudpi_core(self): - """ Load the Core systems for MudPi """ - self.mudpi.load_core() - time.sleep(0.1) - return True - - def initialize_logging(self, config=None): - """ Enable logging module and attach to MudPi """ - config = config or self.mudpi.config.to_dict() - Logger.logger = self.mudpi.logger = Logger(config) - time.sleep(0.05) - Logger.log_formatted( - LOG_LEVEL["info"], "Initializing Logger ", "Complete", 'success' - ) - Logger.log_to_file(LOG_LEVEL["debug"], "Dumping the config file: ") - for index, config_item in config.items(): - Logger.log_to_file(LOG_LEVEL["debug"], f'{index}: {config_item}') - Logger.log_to_file(LOG_LEVEL["debug"], "End of config file dump!\n") - - def load_all_extensions(self, config=None): - """ Import extensions for MudPi based on loaded Config """ - config = config or self.mudpi.config.config - - if not self.import_config_dir(): - raise ConfigError("Could not import the config_path and load extensions") - - - Logger.log_formatted( - LOG_LEVEL["warning"], "Detecting Configurations", "Pending", 'notice' - ) - - core_configs = ['mudpi', 'logging', 'debug'] - # Get all the non-core extensions to load - extensions_to_load = [ - key - for key in config.keys() - if key not in core_configs - ] - Logger.log_formatted( - LOG_LEVEL["warning"], f"Detected {len(extensions_to_load)} Non-Core Configurations", "Complete", 'success' - ) - - Logger.log_formatted( - LOG_LEVEL["warning"], f"Preparing {len(extensions_to_load)} Configurations to be Loaded ", "Pending", 'notice' - ) - - extension_count = len(extensions_to_load) - extension_error_count = 0 - extensions_with_errors = [] - _extensions_needing_load = extensions_to_load - _importer_cache = {} - disabled_cache = self.mudpi.cache.setdefault("disabled_namespaces", {}) - # A loop to fetch all extensions and their dependencies - while _extensions_needing_load: - _extension_load_list = _extensions_needing_load.copy() - _extensions_needing_load = [] - - extension_importers = [] - - # Load extension importers for detected configs - for key in _extension_load_list: - try: - extension_importer = importer.get_extension_importer(self.mudpi, key) - except ExtensionNotFound as error: - extension_importer = None - extensions_to_load.remove(key) - extension_error_count += 1 - extensions_with_errors.append(key) - disabled_cache[key] = 'Not Found' - - if isinstance(extension_importer, importer.ExtensionImporter): - extension_importers.append(extension_importer) - - # Loop through importers and prepare extensions for setup - for extension_importer in extension_importers: - # Load dependenies if there are any - if not extension_importer.dependencies_ready: - extension_importer.import_dependencies() - - # Check if all the component dependencies imported - if extension_importer.dependencies_ready: - _importer_cache[extension_importer.namespace] = extension_importer - - for dependency in extension_importer.loaded_dependencies: - if dependency in extensions_to_load: - continue - - extensions_to_load.append(dependency) - _extensions_needing_load.append(dependency) - - if extension_error_count > 0: - Logger.log_formatted( - LOG_LEVEL["warning"], f"Errors Preparing {extension_error_count} Configurations ", "Errors", 'error' - ) - Logger.log( - "debug", - f'Failed to prepare: {FONT_RED}{", ".join(extensions_with_errors)}{FONT_RESET}' - ) - - if len(extensions_to_load): - Logger.log_formatted( - LOG_LEVEL["warning"], f"{len(extensions_to_load)} Configurations Ready to Load ", "Complete", 'success' - ) - - Logger.log(LOG_LEVEL["debug"], f'{" Load Extensions ":_^{FONT_PADDING+8}}') - Logger.log_formatted( - LOG_LEVEL["warning"], f"Loading {len(extensions_to_load)} Configurations into Extensions ", "Pending", 'notice' - ) - Logger.log( - LOG_LEVEL["debug"], - f'Loading: {FONT_YELLOW}{", ".join(extensions_to_load)}{FONT_RESET}' - ) - - # Import and setup the extensions - self.load_extensions(extensions_to_load, config) - - return self.mudpi.extensions.all() - - def load_extensions(self, extensions, config): - """ Initialize a list of extensions with provided config """ - - disabled_cache = self.mudpi.cache.setdefault("disabled_namespaces", {}) - - # Import and setup the extensions - for extension in extensions: - if extension not in disabled_cache: - try: - extension_importer = importer.get_extension_importer(self.mudpi, extension) - if not extension_importer.import_extension(config): - disabled_cache[extension] = 'Failed Import' - except Exception as error: - # Ignore errors - Logger.log( - LOG_LEVEL["debug"], error - ) - continue - return True - - def import_config_dir(self): - """ Add config dir to sys path so we can import extensions """ - if self.mudpi.config_path is None: - Logger.log( - LOG_LEVEL["error"], - f'{RED_BACK}Could not import config_path - No path was set.{FONT_RESET}' - ) - return False - if self.mudpi.config_path not in sys.path: - sys.path.insert(0, self.mudpi.config_path) - return True - - def validate_config(self, config_path): - """ Validate that config path was provided and a file """ - if not os.path.exists(config_path): - raise ConfigNotFoundError(f"Config File Doesn't Exist at {config_path}") - return False - else: - # No config file provided just a path - pass - - return True - - def debug_dump(self, cache_dump=False): - """ Dump important data from MudPi instance for debugging mode """ - if cache_dump: - Logger.log( - LOG_LEVEL["debug"], - f'{YELLOW_BACK}MUDPI CACHE DUMP{FONT_RESET}' - ) - for key in self.mudpi.cache.keys(): - Logger.log( - LOG_LEVEL["debug"], - f"{FONT_YELLOW}{key}:{FONT_RESET} {self.mudpi.cache[key]}" - ) - - Logger.log( - LOG_LEVEL["debug"], - f'{YELLOW_BACK}MUDPI LOADED EXTENSIONS{FONT_RESET}' - ) - for ext in self.mudpi.extensions.all(): - ext = self.mudpi.cache.get("extension_importers", {}).get(ext) - Logger.log( - LOG_LEVEL["debug"], - f"Namespace: {FONT_YELLOW}{ext.namespace}{FONT_RESET}\n{ext.description}\n{ext.documentation or 'https://mudpi.app/docs'}" - ) - - Logger.log( - LOG_LEVEL["debug"], - f'{YELLOW_BACK}MUDPI DISABLED EXTENSIONS{FONT_RESET}' - ) - for key, reason in self.mudpi.cache.get('disabled_namespaces', {}).items(): - Logger.log( - LOG_LEVEL["debug"], - f"{FONT_YELLOW}{key:<12}{FONT_RESET} {reason}" - ) - - if self.mudpi.components.all(): - Logger.log( - LOG_LEVEL["debug"], - f'{YELLOW_BACK}MUDPI REGISTERED COMPONENTS{FONT_RESET}' - ) - Logger.log( - LOG_LEVEL["debug"], - f"{'COMPONENT':<16} {'ID':<16} NAME\n{'':-<60}" - ) - for namespace, comps in self.mudpi.components.items(): - for comp in comps.values(): - Logger.log( - LOG_LEVEL["debug"], - f"{comp.__class__.__name__:<16} | {comp.id:<16} | {comp.name}" - ) - - if self.mudpi.actions.all(): - Logger.log( - LOG_LEVEL["debug"], - f'{YELLOW_BACK}MUDPI REGISTERED ACTIONS{FONT_RESET}' - ) - Logger.log( - LOG_LEVEL["debug"], - f"{'ACTION CALL':<48} {'ACTION':<32}\n{'':-<80}" - ) - for namespace, actions in self.mudpi.actions.items(): - for key, action in actions.items(): - action_command = f"{namespace or ''}.{key}" - Logger.log( - LOG_LEVEL["debug"], - f"{action_command:<48} | {key:<32}" - ) - - print(f'{"":_<{FONT_PADDING+8}}') \ No newline at end of file diff --git a/build/lib/mudpi/managers/extension_manager.py b/build/lib/mudpi/managers/extension_manager.py deleted file mode 100644 index 9170ede..0000000 --- a/build/lib/mudpi/managers/extension_manager.py +++ /dev/null @@ -1,224 +0,0 @@ -import inspect -from mudpi import importer, extensions -from mudpi.exceptions import MudPiError -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.constants import DEFAULT_UPDATE_INTERVAL - -class ExtensionManager: - """ Extension Manager - - Controls components and interfaces for an extensions. - Helps setup new interfaces and coordinate workers for - any components. Not all extensions need the manager. - """ - - def __init__(self, extension): - self.extension = extension - self.mudpi = extension.mudpi - self.namespace = extension.namespace - self.update_interval = extension.update_interval if extension.update_interval \ - is not None else DEFAULT_UPDATE_INTERVAL - # Config gets set in the `init()` because not all extensions have base config - self.config = None - self.interfaces = {} - # Create an default interface for the extension components without interfaces - self.create_interface(self.namespace) - self.importer = importer.get_extension_importer(self.mudpi, self.namespace) - - self.mudpi.cache.setdefault('extension_managers', {})[self.namespace] = self - - """ Properties """ - @property - def components(self): - """ Returns a list of all components for all interfaces """ - return [ component - for interface in self.interfaces.values() - for worker in interface.workers - for component in worker.components ] - - """ Methods """ - def init(self, config, load_interfaces=True): - """ Parses the config and setups up any interfaces detected """ - self.config = config - if load_interfaces: - self.load_interfaces(config) - - def load_interfaces(self, config): - """ Load interfaces from config """ - for conf in config: - if not conf: - continue - - if not isinstance(conf, list): - conf = [conf] - - for entry in conf: - try: - # Search for interface property - interface = entry.get("interface") - except AttributeError as error: - interface = None - - if interface is None: - continue - - _interface = None - try: - _interface = self.find_or_create_interface(interface, entry) - if not _interface: - raise MudPiError(f'Interface {interface} failed to load.') - except MudPiError as error: - Logger.log( - LOG_LEVEL["debug"], f"Extension Manager {self.namespace}:{interface} {error}." - ) - continue - - if _interface.__class__.load == _interface.__class__.__bases__[0].load: - Logger.log( - LOG_LEVEL["debug"], f"Extension {self.namespace} Interface {_interface_name} did not define load() method." - ) - result = _interface.load(entry) - if not result: - Logger.log( - LOG_LEVEL["error"], f'Interface {self.namespace}.{self.interface_name} `load()` failed to return True.' - ) - return True - - def create_interface(self, interface_name, interface=None, update_interval=None, extension=None): - """ Create a new interface manager and return it """ - cache = self.mudpi.cache.setdefault("interfaces", {}) - - if interface: - if not hasattr(interface, 'Interface'): - raise MudPiError(f'No `Interface()` class to load for {self.namespace}:{interface_name}.') - - if update_interval is None: - if interface: - if interface.Interface.update_interval is not None: - update_interval = interface.Interface.update_interval - else: - update_interval = self.update_interval - - key = f'{self.namespace}.{interface_name}.{update_interval}' - - if key in cache: - return cache[key] - - if interface: - if _is_interface(interface.Interface): - cache[key] = interface.Interface(self.mudpi, self.namespace, interface_name, update_interval) - # Inject the extension here since its not needed in the init during validation - cache[key].extension = extension or self.extension - return cache[key] - else: - raise MudPiError(f'Interface {self.namespace}:{interface_name} does not extend BaseInterface.') - - cache[key] = extensions.BaseInterface(self.mudpi, self.namespace, interface_name, update_interval) - # Inject the extension here since its not needed in the init during validation - cache[key].extension = extension or self.extension - self.interfaces[key] = cache[key] - return cache[key] - - def find_or_create_interface(self, interface_name, interface_config = {}): - """ Add an interface for an Extension if it isn't loaded """ - - if self.config is None: - raise MudPiError("Config was null in extension manager. Call `init(config)` first.") - return - - # Get the interface and extension - interface, extension = self.importer.prepare_interface_and_import(interface_name) - - if not interface: - raise MudPiError(f'Interface {interface_name} failed to prepare for import.') - - update_interval = interface_config.get('update_interval') - - if not update_interval: - if hasattr(interface, 'Interface'): - if interface.Interface.update_interval is not None: - update_interval = interface.Interface.update_interval - - if not update_interval: - update_interval = self.update_interval - - # Create a composite key based on interface and update intervals - interface_key = f'{self.namespace}.{interface_name}.{update_interval}' - - # Check if interface is already added - if interface_key in self.interfaces: - return self.interfaces[interface_key] - - self.interfaces[interface_key] = self.create_interface(interface_name, interface, update_interval, extension=extension) - return self.interfaces[interface_key] - - def add_component(self, component, interface_name=None): - """ Delegate register component using the specified interface. """ - interface_name = interface_name or self.namespace - interface_key = f'{self.namespace}.{interface_name}.{self.update_interval}' - if interface_key not in self.interfaces: - raise MudPiError(f"Attempted to add_component to interface {interface_key} that doesn't exist.") - - return self.interfaces[interface_key].add_component(component) - - def register_component_actions(self, action_key, action): - """ Register action for component. - If no components specified it calls all components - """ - - def handle_namespace_action(data=None): - """ Wrapper for action call to delegate to components """ - _components = [] - _ids = [] - if data: - try: - _ids = data.get('components', []) - except Exception as error: - _ids = [] - if _ids: - for _id in _ids: - component = self.mudpi.components.get(_id) - if component: - _components.append(component) - else: - _components = self.mudpi.components.for_namespace(self.namespace).values() - - for component in _components: - try: - func = getattr(component, action) - if callable(func): - if data: - func(data) - else: - func() - except Exception as error: - continue - return True - - for component in self.mudpi.components.for_namespace(self.namespace).values(): - try: - func = getattr(component, action) - if callable(func): - # Register component only global action - self.mudpi.actions.register(f'{component.id}.{action_key}', func) - except Exception as error: - continue - - self.mudpi.actions.register(action_key, handle_namespace_action, self.namespace) - - def __repr__(self): - """ Representation of the manager. (Handy for debugging) """ - return f"" - - -""" Helper """ -def _is_interface(cls): - """ Check if a class is a MudPi Extension. - Accepts class or instance of class - """ - if not inspect.isclass(cls): - if hasattr(cls, '__class__'): - cls = cls.__class__ - else: - return False - return issubclass(cls, extensions.BaseInterface) \ No newline at end of file diff --git a/build/lib/mudpi/managers/state_manager.py b/build/lib/mudpi/managers/state_manager.py deleted file mode 100644 index 6c1d954..0000000 --- a/build/lib/mudpi/managers/state_manager.py +++ /dev/null @@ -1,157 +0,0 @@ -import json -import redis -import datetime -import threading - -from mudpi.constants import FONT_RESET, FONT_YELLOW -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class StateManager: - """ - A Central Manager to Control All States in MudPi. - - It will keep a sync of state with redis so that data - can be recovered on restart and shared with the frontend. - """ - - def __init__(self, mudpi, redis_conf=None): - self.mudpi = mudpi - self.states = {} - self._lock = threading.Lock() - host = '127.0.0.1' - port = 6379 - try: - if redis_conf: - host = redis_conf.get('host', '127.0.0.1') - port = redis_conf.get('port', 6379) - self.redis = redis.Redis(host=host, port=port) - except Exception as error: - Logger.log(LOG_LEVEL["error"], - f"State Manager Error Connecting to Redis") - - self.restore_states() - - Logger.log_formatted(LOG_LEVEL["info"], - f"Preparing State Manager ", "Complete", "success") - - def get(self, id): - return self.states.get(id.lower()) - - def all(self): - with self._lock: - return list(self.states.values()) - - def remove(self, id): - with self._lock: - return self.states.pop(id.lower(), None) - - def id_exists(self, _id): - _id = _id.lower() - return _id in self.states - - def set(self, component_id, new_state, metadata=None): - if new_state is None: - return - - component_id = component_id.lower() - new_state = json.dumps(new_state) - metadata = metadata or {} - - with self._lock: - previous_state = self.states.get(component_id) - - state_exists = previous_state is not None - state_is_same = state_exists and previous_state.state == new_state - metadata_is_same = state_exists and previous_state.metadata == metadata - - if state_is_same and metadata_is_same: - return - - updated_at = previous_state.updated_at if state_is_same else None - - state = State(component_id, new_state, metadata, updated_at) - self.states[component_id] = state - - if previous_state: - previous_state = previous_state.to_dict() - event_data = { - 'event': 'StateUpdated', - 'component_id': component_id, - 'previous_state': previous_state, - 'new_state': state.to_dict() - } - - self.mudpi.events.publish('state', event_data) - self.redis.set(f'{component_id}.state', json.dumps(state.to_dict())) - self.redis.set('state_keys', json.dumps(self.ids())) - Logger.log(LOG_LEVEL["debug"], - f"State Changed: {FONT_YELLOW}{component_id}{FONT_RESET} - {state.state} @ {state.updated_at}") - return event_data - - def ids(self): - """ Return the keys of all the stored states """ - return [ - item.component_id - for item in self.states.values() - ] - - def restore_states(self): - """ Restore states from Redis - Resumes state of previous run if system has not cleared memory. - """ - keys = self.redis.get('state_keys') - if keys: - keys = json.loads(keys) - for key in keys: - data = self.redis.get(f'{key}.state') - _state = State.from_json(data) - self.states[key] = _state - - -class State: - """ - A Class for Stored State from Components - """ - @classmethod - def from_json(cls, data): - parsed_data = json.loads(data) - return cls(parsed_data['component_id'], parsed_data['state'], parsed_data.get('metadata'), parsed_data['updated_at'], parsed_data.get('source_id')) - - def __init__( - self, - component_id, - state = {}, - metadata = {}, - updated_at = datetime.datetime.now(), - source_id = None - ): - self.component_id = component_id - self.state = state - self.metadata = metadata # Used for UI like icons, measure units, and display names. - self.updated_at = updated_at if updated_at is not None else datetime.datetime.now().replace(microsecond=0) - self.source_id = source_id - - @property - def name(self): - return self.metadata.get('name', "Unknown") - - def to_dict(self): - return { - 'component_id':self.component_id, - 'state': self.state, - 'metadata': self.metadata, - 'updated_at': str(self.updated_at), - 'source_id': self.source_id - } - - def __eq__(self, other): - """ Provide a way to check if 'state == state'. """ - return (self.__class__ == other.__class__ and - self.component_id == other.component_id and - self.state == other.state and - self.metadata == other.metadata) - - def __repr__(self): - """ Representation of state. (Handy for debugging) """ - return f"" diff --git a/build/lib/mudpi/mudpi_main.py b/build/lib/mudpi/mudpi_main.py deleted file mode 100644 index 2cf10e4..0000000 --- a/build/lib/mudpi/mudpi_main.py +++ /dev/null @@ -1,293 +0,0 @@ -""" 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. -""" - -import time -import json -import redis -import socket -import threading -import datetime - -from adafruit_platformdetect import Detector - -from . import constants -from action import Action - -from server.mudpi_server import MudpiServer -from workers.linux.lcd_worker import LcdWorker -from workers.trigger_worker import TriggerWorker -from workers.sequence_worker import SequenceWorker -from workers.linux.relay_worker import RelayWorker -from workers.linux.i2c_worker import LinuxI2CWorker -from workers.linux.sensor_worker import LinuxSensorWorker -from workers.linux.control_worker import LinuxControlWorker - -try: - from workers.arduino.arduino_worker import ArduinoWorker - - NANPY_ENABLED = True -except ImportError: - NANPY_ENABLED = False - -try: - from workers.adc_worker import ADCMCP3008Worker - - MCP_ENABLED = True -except (ImportError, AttributeError): - MCP_ENABLED = False - -from logger.Logger import Logger, LOG_LEVEL - -detector = Detector() -if detector.board.any_raspberry_pi: - from workers.linux.camera_worker import CameraWorker - -PROGRAM_RUNNING = True - -# Variables and LOAD CONFIG -# TODO: REPLACE WITH THE NEW LOGIC HERE - -# Print a display logo for startup - -# Load Configs - -# Debug Check - -# Load the Logger - -try: - # Prepare Core and Threads - - # 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 = LinuxSensorWorker( - 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 = LinuxControlWorker( - 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 = LinuxI2CWorker(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 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 -finally: - 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/build/lib/mudpi/registry.py b/build/lib/mudpi/registry.py deleted file mode 100644 index 54856b2..0000000 --- a/build/lib/mudpi/registry.py +++ /dev/null @@ -1,172 +0,0 @@ -import json -from mudpi.exceptions import MudPiError - - -class Registry: - """ Key-Value database for managing object instances """ - def __init__(self, mudpi, name): - self.mudpi = mudpi - self.name = name - self._registry = {} - - def all(self): - """ Return all items in the registry """ - return self._registry - - def items(self): - """ Dict items() helper for iteration """ - return self.all().items() - - def keys(self): - """ Dict keys() helper for iteration """ - return self.all().keys() - - def get(self, key): - """ Get an item for the specified key """ - return self._registry[key] - - def exists(self, key): - """ Return if key exists in the registry """ - return key in self._registry - - def register(self, key, value): - """ Registers the value into the registry """ - if key not in self._registry: - self.mudpi.events.publish(self.name, {'event': 'Registered', 'action': key}) - self._registry[key] = value - return value - - @property - def length(self): - return len(self.all()) - - -class ComponentRegistry(Registry): - """ Comopnent Database - Stores components per namespace for MudPi - """ - def get(self, component_id): - """ Get an item for the specified key """ - try: - component = [ component - for components in self._registry.values() - for _id, component in components.items() - if _id in component_id ][0] - except Exception as error: - component = None - return component - - def for_namespace(self, namespace=None): - """ Get all the components for a given namespace """ - return self._registry.setdefault(namespace, {}) - - def exists(self, component_ids): - """ Return if key exists in the registry """ - return any([ exists for components in self._registry.values() - for exists in components - if exists in component_ids ]) - - def register(self, component_id, component, namespace=None): - """ Registers the component into the registry """ - namespace_registry = self._registry.setdefault(namespace, {}) - if component_id not in namespace_registry: - self.mudpi.events.publish('core', {'event': 'ComponentRegistered', 'component': component_id, 'namespace': namespace}) - namespace_registry[component_id] = component - return component - - def ids(self): - """ Return all the registered component ids """ - return [ component.id - for components in self._registry.values() - for component in components.values() ] - - -class ActionRegistry(Registry): - """ Database of actions available to MudPi from - user configs or components. - None = global - """ - def register(self, action_key, func, namespace=None, validator=None): - """ Register the action under the specified namespace. """ - namespace_registry = self._registry.setdefault(namespace, {}) - if action_key not in namespace_registry: - self.mudpi.events.publish('core', {'event': 'ActionRegistered', 'action': action_key, 'namespace': namespace}) - namespace_registry[action_key] = Action(func, validator) - - def for_namespace(self, namespace=None): - """ Get all the actions for a given namespace """ - return self._registry.setdefault(namespace, {}) - - def exists(self, action_key): - """ Return if action exists for given action command """ - action = self.parse_call(action_key) - registry = self._registry.setdefault(action['namespace'], {}) - return action['action'] in registry - - def parse_call(self, action_call): - """ Parse a command string and extract the namespace and action """ - parsed_action = {} - if action_call.startswith('.'): - # Empty Namespace - parsed_action['namespace'] = None - action_call = action_call.replace('.', '', 1) - parsed_action['action'] = action_call - elif '.' in action_call: - parts = action_call.split('.', 1) - parsed_action['namespace'] = parts[0] - parsed_action['action'] = parts[1] - else: - parsed_action['namespace'] = None - parsed_action['action'] = action_call - return parsed_action - - def call(self, action_call, namespace=None, action_data={}): - """ Call an action from the registry - Format: {namespace}.{action} or - {namespace}.{component}.{action} - """ - command = self.parse_call(action_call) - action = self._registry.get(command['namespace'], {}).get(command['action']) - if not action: - raise MudPiError("Call to action that doesn't exists!") - validated_data = action.validate(action_data) - if not validated_data and action_data: - raise MudPiError("Action data was not valid!") - self.mudpi.events.publish('core', {'event': 'ActionCall', 'action': action_call, 'data': action_data, 'namespace': namespace or command['namespace']}) - action(data=validated_data) - - - def handle_call(self, event_data={}): - """ Handle an Action call from event bus """ - if event_data: - try: - _data = json.loads(event_data.get('data', {})) - except Exception: - _data = event_data - action = _data.get('action') - if action: - return self.call(action, _data.get('namespace'), _data.get('data', {})) - -class Action: - """ A callback associated with a string """ - - def __init__(self, func, validator): - self.func = func - self.validator = None - - def validate(self, data): - if not self.validator: - return data - - if callable(self.validator): - return self.validator(data) - - return False - - def __call__(self, data=None, **kwargs): - if self.func: - if callable(self.func): - if data: - return self.func(data) - else: - return self.func() \ No newline at end of file diff --git a/build/lib/mudpi/sensors/__init__.py b/build/lib/mudpi/sensors/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/sensors/arduino/__init__.py b/build/lib/mudpi/sensors/arduino/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/sensors/arduino/float_sensor.py b/build/lib/mudpi/sensors/arduino/float_sensor.py deleted file mode 100644 index 344cae2..0000000 --- a/build/lib/mudpi/sensors/arduino/float_sensor.py +++ /dev/null @@ -1,25 +0,0 @@ -from nanpy import (SerialManager) - -from mudpi.sensors.arduino.sensor import Sensor - -default_connection = SerialManager(device='/dev/ttyUSB0') - - -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) - - 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_raw(self): - return self.read() diff --git a/build/lib/mudpi/sensors/arduino/humidity_sensor.py b/build/lib/mudpi/sensors/arduino/humidity_sensor.py deleted file mode 100644 index 0d74b88..0000000 --- a/build/lib/mudpi/sensors/arduino/humidity_sensor.py +++ /dev/null @@ -1,52 +0,0 @@ -import time -import datetime -import json -import redis -from .sensor import Sensor -from nanpy import (ArduinoApi, SerialManager, DHT) -import sys - - - -import constants - -default_connection = SerialManager(device='/dev/ttyUSB0') - - -# 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 read_raw(self): - return self.read() diff --git a/build/lib/mudpi/sensors/arduino/light_sensor.py b/build/lib/mudpi/sensors/arduino/light_sensor.py deleted file mode 100644 index 90f35c8..0000000 --- a/build/lib/mudpi/sensors/arduino/light_sensor.py +++ /dev/null @@ -1,36 +0,0 @@ -import time -import datetime -import json -import redis -from .sensor import Sensor -from nanpy import (ArduinoApi, SerialManager) -import sys - - - -import constants - -default_connection = SerialManager(device='/dev/ttyUSB0') - - -# 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_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_raw(self): - return self.read() diff --git a/build/lib/mudpi/sensors/arduino/rain_sensor.py b/build/lib/mudpi/sensors/arduino/rain_sensor.py deleted file mode 100644 index edb386a..0000000 --- a/build/lib/mudpi/sensors/arduino/rain_sensor.py +++ /dev/null @@ -1,78 +0,0 @@ -import time -import datetime -import json -import redis -from .sensor import Sensor -from nanpy import (ArduinoApi, SerialManager) -import sys - - - -import constants - -default_connection = SerialManager(device='/dev/ttyUSB0') -# 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 -MIST_BOUNDS = 800 -LIGHT_RAIN_BOUNDS = 750 -RAIN_BOUNDS = 550 -HEAVY_RAIN_BOUNDS = 400 -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_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_raw(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 MIST_BOUNDS >= raw_data > LIGHT_RAIN_BOUNDS: - return 'Mist' - elif LIGHT_RAIN_BOUNDS >= raw_data > RAIN_BOUNDS: - return 'Light Rain' - elif RAIN_BOUNDS >= raw_data > HEAVY_RAIN_BOUNDS: - return 'Rain' - elif HEAVY_RAIN_BOUNDS >= 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...') diff --git a/build/lib/mudpi/sensors/arduino/sensor.py b/build/lib/mudpi/sensors/arduino/sensor.py deleted file mode 100644 index f298bb3..0000000 --- a/build/lib/mudpi/sensors/arduino/sensor.py +++ /dev/null @@ -1,55 +0,0 @@ -import time -import json -import redis -from nanpy import (ArduinoApi, SerialManager) -from mudpi.sensors.base_sensor import BaseSensor - -default_connection = SerialManager() - - -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/build/lib/mudpi/sensors/arduino/soil_sensor.py b/build/lib/mudpi/sensors/arduino/soil_sensor.py deleted file mode 100644 index 25d980c..0000000 --- a/build/lib/mudpi/sensors/arduino/soil_sensor.py +++ /dev/null @@ -1,57 +0,0 @@ -import time -import datetime -import json -import redis -from .sensor import Sensor -from nanpy import (ArduinoApi, SerialManager) -import sys - - - -import constants - -default_connection = SerialManager(device='/dev/ttyUSB0') -# 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); - - -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 80 >= moistpercent > 45: - moisture = 'Dry - ' + str(int(moistpercent)) - elif 45 >= 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/build/lib/mudpi/sensors/arduino/temperature_sensor.py b/build/lib/mudpi/sensors/arduino/temperature_sensor.py deleted file mode 100644 index ff8686e..0000000 --- a/build/lib/mudpi/sensors/arduino/temperature_sensor.py +++ /dev/null @@ -1,76 +0,0 @@ -import time -import datetime -import json -import redis -from .sensor import Sensor -from nanpy import (DallasTemperature, SerialManager) -import sys - -from logger.Logger import Logger, LOG_LEVEL - - - -import constants - -default_connection = SerialManager(device='/dev/ttyUSB0') - - -# 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 - - -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...') diff --git a/build/lib/mudpi/sensors/base_sensor.py b/build/lib/mudpi/sensors/base_sensor.py deleted file mode 100644 index 50d9cf5..0000000 --- a/build/lib/mudpi/sensors/base_sensor.py +++ /dev/null @@ -1,35 +0,0 @@ -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/build/lib/mudpi/sensors/linux/__init__.py b/build/lib/mudpi/sensors/linux/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/sensors/linux/float_sensor.py b/build/lib/mudpi/sensors/linux/float_sensor.py deleted file mode 100644 index 2323658..0000000 --- a/build/lib/mudpi/sensors/linux/float_sensor.py +++ /dev/null @@ -1,32 +0,0 @@ -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/build/lib/mudpi/sensors/linux/humidity_sensor.py b/build/lib/mudpi/sensors/linux/humidity_sensor.py deleted file mode 100644 index 85a3c67..0000000 --- a/build/lib/mudpi/sensors/linux/humidity_sensor.py +++ /dev/null @@ -1,104 +0,0 @@ -import json -import sys -import time -import board -import adafruit_dht - -from sensors.linux.sensor import Sensor - -from logger.Logger import Logger, LOG_LEVEL - -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 - - try: - self.dht_device = self.sensor(self.pin_obj) - Logger.log( - LOG_LEVEL["debug"], - 'Sensor Initializing: DHT' - ) - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - 'Sensor Initialize Error: DHT Failed to Init' - ) - 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 - - try: - # Calling temperature or humidity triggers measure() - temperature_c = self.dht_device.temperature - humidity = self.dht_device.humidity - except RuntimeError as error: - # Errors happen fairly often, DHT's are hard to read - Logger.log(LOG_LEVEL["error"], error) - time.sleep(2) - except Exception as error: - Logger.log( - LOG_LEVEL["error"], - 'DHT Device Encountered an Error.' - ) - self.dht_device.exit() - - 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/build/lib/mudpi/sensors/linux/i2c/__init__.py b/build/lib/mudpi/sensors/linux/i2c/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/sensors/linux/i2c/bme680_sensor.py b/build/lib/mudpi/sensors/linux/i2c/bme680_sensor.py deleted file mode 100644 index 649762b..0000000 --- a/build/lib/mudpi/sensors/linux/i2c/bme680_sensor.py +++ /dev/null @@ -1,57 +0,0 @@ -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/build/lib/mudpi/sensors/linux/i2c/sensor.py b/build/lib/mudpi/sensors/linux/i2c/sensor.py deleted file mode 100644 index c7bb4c2..0000000 --- a/build/lib/mudpi/sensors/linux/i2c/sensor.py +++ /dev/null @@ -1,53 +0,0 @@ -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/build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py b/build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py deleted file mode 100644 index 4cff16a..0000000 --- a/build/lib/mudpi/sensors/linux/i2c/t9602_sensor.py +++ /dev/null @@ -1,53 +0,0 @@ -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/build/lib/mudpi/sensors/linux/sensor.py b/build/lib/mudpi/sensors/linux/sensor.py deleted file mode 100644 index 0a49947..0000000 --- a/build/lib/mudpi/sensors/linux/sensor.py +++ /dev/null @@ -1,57 +0,0 @@ -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/build/lib/mudpi/sensors/mcp3xxx/__init__.py b/build/lib/mudpi/sensors/mcp3xxx/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/sensors/mcp3xxx/sensor.py b/build/lib/mudpi/sensors/mcp3xxx/sensor.py deleted file mode 100644 index 6b6a4b6..0000000 --- a/build/lib/mudpi/sensors/mcp3xxx/sensor.py +++ /dev/null @@ -1,45 +0,0 @@ -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/build/lib/mudpi/sensors/mcp3xxx/soil_sensor.py b/build/lib/mudpi/sensors/mcp3xxx/soil_sensor.py deleted file mode 100644 index 7911c99..0000000 --- a/build/lib/mudpi/sensors/mcp3xxx/soil_sensor.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys - -from adafruit_mcp3xxx.analog_in import AnalogIn - -from logger.Logger import Logger, LOG_LEVEL -from sensors.mcp3xxx.sensor import Sensor - - - -# Tested using Sun3Drucker Model SX239 -# Wet Water = 287 -# Dry Air = 584 -AirBounds = 43700 -WaterBounds = 13000 -intervals = int((AirBounds - WaterBounds) / 3) - - -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) - return - - def init_sensor(self): - self.topic = AnalogIn(self.mcp, Sensor.PINS[self.pin]) - - def read(self): - resistance = self.read_pin() - moistpercent = ( - (resistance - WaterBounds) / ( - AirBounds - WaterBounds) - ) * 100 - if moistpercent > 80: - moisture = 'Very Dry - ' + str(int(moistpercent)) - elif 80 >= moistpercent > 45: - moisture = 'Dry - ' + str(int(moistpercent)) - elif 45 >= 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) - - Logger.log(LOG_LEVEL["debug"], "moisture: {0}".format(moisture)) - return resistance diff --git a/build/lib/mudpi/utils.py b/build/lib/mudpi/utils.py deleted file mode 100644 index d516fa9..0000000 --- a/build/lib/mudpi/utils.py +++ /dev/null @@ -1,102 +0,0 @@ -import sys -import json -import socket -import inspect -import subprocess -from mudpi.extensions import Component, BaseExtension, BaseInterface - -def get_ip(): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - # doesn't even have to be reachable - s.connect(('10.255.255.255', 1)) - IP = s.getsockname()[0] - except Exception: - IP = '127.0.0.1' - finally: - s.close() - return IP - - -def get_module_classes(module_name): - """ Get all the classes from a module """ - clsmembers = inspect.getmembers(sys.modules[module_name], inspect.isclass) - return clsmembers - - -def decode_event_data(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 as error: - # print('Json Error. Str Found') - return message.decode('utf-8') #{'event': 'Unknown', 'data': message} - else: - # print('Failed to detect type') - return {'event': 'Unknown', 'data': message} - - -def install_package(package, upgrade=False, target=None): - """ - Install a PyPi package with pip in the background. - Returns boolean. - """ - pip_args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] - if upgrade: - pip_args.append('--upgrade') - if target: - pip_args += ['--target', os.path.abspath(target)] - try: - return 0 == subprocess.call(pip_args) - except subprocess.SubprocessError: - return False - - -def is_package_installed(package): - """ Check if a package is already installed """ - reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) - - installed_packages = [r.decode().split('==')[0].lower() for r in reqs.split()] - - return package in installed_packages - - -def is_extension(cls): - """ Check if a class is a MudPi Extension. - Accepts class or instance of class - """ - if not inspect.isclass(cls): - if hasattr(cls, '__class__'): - cls = cls.__class__ - else: - return False - return issubclass(cls, BaseExtension) - - -def is_interface(cls): - """ Check if a class is a MudPi Extension. - Accepts class or instance of class - """ - if not inspect.isclass(cls): - if hasattr(cls, '__class__'): - cls = cls.__class__ - else: - return False - return issubclass(cls, BaseInterface) - - -def is_component(cls): - """ Check if a class is a MudPi component. - Accepts class or instance of class - """ - if not inspect.isclass(cls): - if hasattr(cls, '__class__'): - cls = cls.__class__ - else: - return False - return issubclass(cls, Component) \ No newline at end of file diff --git a/build/lib/mudpi/workers/__init__.py b/build/lib/mudpi/workers/__init__.py deleted file mode 100644 index 4515fd2..0000000 --- a/build/lib/mudpi/workers/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -import time -import redis -import threading -from uuid import uuid4 - -from mudpi import constants -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class Worker: - """ Base Worker Class - - A worker is responsible for managing components, - updating component state, configurations ,etc. - - A worker runs on a thread with an interruptable sleep - interaval between update cycles. - """ - def __init__(self, mudpi, config): - self.mudpi = mudpi - self.config = config - self.components = {} - - if self.key is None: - self.config['key'] = f'{self.__class__.__name__}-{uuid4()}' - - self._worker_available = threading.Event() - self._thread = None - self.init() - self.mudpi.workers.register(self.key, self) - - """ Properties """ - @property - def key(self): - return self.config.get('key') - - @property - def update_interval(self): - return self.config.get('update_interval', constants.DEFAULT_UPDATE_INTERVAL) - - @property - def is_available(self): - return self._worker_available.is_set() - - @is_available.setter - def is_available(self, value): - if bool(value): - self._worker_available.set() - else: - self._worker_available.clear() - - """ Methods """ - def init(self): - """ Called at end of __init__ for additonal setup """ - pass - - def run(self, func=None): - if not self._thread: - self._thread = threading.Thread(target=self.work, args=(func,)) - Logger.log_formatted(LOG_LEVEL["debug"], - f"Worker {self.key} ", "Starting", "notice") - self._thread.start() - Logger.log_formatted(LOG_LEVEL["info"], - f"Worker {self.key} ", "Started", "success") - return self._thread - - def work(self, func=None): - """ Perform work each cycle like checking devices, - polling sensors, or listening to events. - Worker should sleep based on `update_interval` - """ - while self.mudpi.is_prepared: - if self.mudpi.is_running: - if callable(func): - func() - for key, component in self.components.items(): - if component.should_update: - component.update() - component.store_state() - - self._wait(self.update_interval) - # # MudPi Shutting Down, Perform Cleanup Below - Logger.log_formatted(LOG_LEVEL["debug"], - f"Worker {self.key} ", "Stopping", "notice") - for key, component in self.components.items(): - component.unload() - Logger.log_formatted(LOG_LEVEL["info"], - f"Worker {self.key} ", "Offline", "error") - - def _wait(self, duration=0): - """ Sleeps for a given duration - This allows the worker to be interupted - while waiting. - """ - time_remaining = duration - while time_remaining > 0 and self.mudpi.is_prepared: - time.sleep(1) - time_remaining -= 1 - - """ Should be moved to Timer util """ - @property - 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 diff --git a/build/lib/mudpi/workers/adc_worker.py b/build/lib/mudpi/workers/adc_worker.py deleted file mode 100644 index cf0f4ef..0000000 --- a/build/lib/mudpi/workers/adc_worker.py +++ /dev/null @@ -1,138 +0,0 @@ -import time -import json -import busio -import board -import redis -import digitalio -import threading -import importlib -import adafruit_mcp3xxx.mcp3008 as MCP -from adafruit_mcp3xxx.analog_in import AnalogIn - -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class ADCMCP3008Worker: - """ - Analog-Digital-Converter Worker - """ - PINS = { - '4': board.D4, - '17': board.D17, - '27': board.D27, - '22': board.D22, - '5': board.D5, - '6': board.D6, - '13': board.D13, - '19': board.D19, - '26': board.D26, - '18': board.D18, - '23': board.D23, - '24': board.D24, - '25': board.D25, - '12': board.D12, - '16': board.D16, - '20': board.D20, - '21': board.D21 - } - - def __init__(self, config: dict, main_thread_running, system_ready): - self.config = config - self.main_thread_running = main_thread_running - 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) - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - - spi = busio.SPI(clock=board.SCK, MISO=board.MISO, MOSI=board.MOSI) - cs = digitalio.DigitalInOut(ADCMCP3008Worker.PINS[config['pin']]) - - self.mcp = MCP.MCP3008(spi, cs) - - self.sensors = [] - self.init_sensors() - - self.node_ready = True - - def dynamic_sensor_import(self, path): - components = path.split('.') - - s = '' - for component in components[:-1]: - s += component + '.' - - parent = importlib.import_module(s[:-1]) - sensor = getattr(parent, components[-1]) - - return sensor - - 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( - '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')), - 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) - ) - - 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' - ) - return t - - else: - Logger.log( - LOG_LEVEL["warning"], - "Node Connection...\t\t\t\033[1;31m Failed\033[0;0m" - ) - return None - - def work(self): - - while self.main_thread_running.is_set(): - if self.system_ready.is_set() and self.node_ready: - 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["info"], readings) - message['data'] = readings - self.r.publish('sensors', json.dumps(message)) - - 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) - ) diff --git a/build/lib/mudpi/workers/arduino/__init__.py b/build/lib/mudpi/workers/arduino/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/workers/arduino/arduino_control_worker.py b/build/lib/mudpi/workers/arduino/arduino_control_worker.py deleted file mode 100644 index bd07761..0000000 --- a/build/lib/mudpi/workers/arduino/arduino_control_worker.py +++ /dev/null @@ -1,128 +0,0 @@ -import time -import json -import threading -import random -import socket -from nanpy import (SerialManager) -from nanpy.serialmanager import SerialManagerError -from nanpy.sockconnection import (SocketManager, SocketManagerError) -from .worker import Worker -from controls.arduino.button_control import (ButtonControl) -from controls.arduino.switch_control import (SwitchControl) -from controls.arduino.potentiometer_control import (PotentiometerControl) -import sys - - - -import constants -import importlib -from logger.Logger import Logger, LOG_LEVEL - - -# 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) - ) diff --git a/build/lib/mudpi/workers/arduino/arduino_relay_worker.py b/build/lib/mudpi/workers/arduino/arduino_relay_worker.py deleted file mode 100644 index 24fc412..0000000 --- a/build/lib/mudpi/workers/arduino/arduino_relay_worker.py +++ /dev/null @@ -1,215 +0,0 @@ -import time -import datetime -import json -import redis -import threading -import sys -import socket -from nanpy import (SerialManager, ArduinoApi) -from nanpy.serialmanager import SerialManagerError -from nanpy.sockconnection import (SocketManager, SocketManagerError) -from .worker import Worker - - - -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.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/build/lib/mudpi/workers/arduino/arduino_sensor_worker.py b/build/lib/mudpi/workers/arduino/arduino_sensor_worker.py deleted file mode 100644 index 7d5a9eb..0000000 --- a/build/lib/mudpi/workers/arduino/arduino_sensor_worker.py +++ /dev/null @@ -1,135 +0,0 @@ -import time -import json -import threading -import random -import socket -from .worker import Worker -from nanpy import (SerialManager) -from nanpy.serialmanager import SerialManagerError -from nanpy.sockconnection import (SocketManager, SocketManagerError) -from sensors.arduino.rain_sensor import (RainSensor) -from sensors.arduino.soil_sensor import (SoilSensor) -from sensors.arduino.float_sensor import (FloatSensor) -from sensors.arduino.light_sensor import (LightSensor) -from sensors.arduino.humidity_sensor import (HumiditySensor) -from sensors.arduino.temperature_sensor import (TemperatureSensor) -import sys - - -import importlib -from logger.Logger import Logger, LOG_LEVEL - - -# 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: {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/build/lib/mudpi/workers/arduino/arduino_worker.py b/build/lib/mudpi/workers/arduino/arduino_worker.py deleted file mode 100644 index 26e8887..0000000 --- a/build/lib/mudpi/workers/arduino/arduino_worker.py +++ /dev/null @@ -1,244 +0,0 @@ -import time -import json -import threading -import random -import socket -from nanpy import (SerialManager, ArduinoApi) -from nanpy.serialmanager import SerialManagerError -from nanpy.sockconnection import (SocketManager, SocketManagerError) -from workers.arduino.arduino_control_worker import ArduinoControlWorker -from workers.arduino.arduino_sensor_worker import ArduinoSensorWorker -from workers.arduino.arduino_relay_worker import ArduinoRelayWorker -from .worker import Worker -import sys - - - -import importlib -from logger.Logger import Logger, LOG_LEVEL - - -# 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.relay_events = {} - 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.relay_events[ - 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/build/lib/mudpi/workers/arduino/worker.py b/build/lib/mudpi/workers/arduino/worker.py deleted file mode 100644 index e88f9ef..0000000 --- a/build/lib/mudpi/workers/arduino/worker.py +++ /dev/null @@ -1,75 +0,0 @@ -import time -import datetime -import json -import redis -import threading -import sys - - - -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.api = None - 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 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: - # print('Json Error. Str Found') - return {'event': 'Unknown', 'data': message} - else: - # print('Failed to detect type') - return {'event': 'Unknown', 'data': message} diff --git a/build/lib/mudpi/workers/linux/__init__.py b/build/lib/mudpi/workers/linux/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/mudpi/workers/linux/camera_worker.py b/build/lib/mudpi/workers/linux/camera_worker.py deleted file mode 100644 index cccf055..0000000 --- a/build/lib/mudpi/workers/linux/camera_worker.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -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 os -import sys -import time -import json -import datetime -import threading - -from picamera import PiCamera - -from mudpi.workers import Worker -from mudpi.utils import get_config_item -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.constants import __version__, PATH_CONFIG, DEFAULT_CONFIG_FILE, FONT_RESET_CURSOR, FONT_RESET, YELLOW_BACK, GREEN_BACK, FONT_GREEN, FONT_RED, FONT_YELLOW, FONT_PADDING - - -class CameraWorker(Worker): - def __init__(self, mudpi, config): - super().__init__(mudpi, config) - self.pending_reset = False - - # Events - if self.config.get("thread_events"): - self.camera_available = self.config["thread_events"].get("camera_available") - else: - self.config["thread_events"] = {} - self.camera_available = self.config["thread_events"]["camera_available"] = threading.Event() - - # 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)) - - config = self.config - - self.init() - return - - def init(self): - try: - Logger.log( - LOG_LEVEL["info"], - f'{f"Camera Worker {self.key}":.<{FONT_PADDING}} {FONT_YELLOW}Initializing{FONT_RESET}' - ) - super().init() - 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}) - - 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["warning"], - f"{f'Camera Worker [{self.key}]...':.<{FONT_PADDING}} {FONT_GREEN}Working{FONT_RESET}") - 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.config["thread_events"]["camera_available"].clear() - self.pending_reset = True - except Exception: - Logger.log(LOG_LEVEL["error"], - 'Error Handling Event for Camera') - - def listen(self): - while self.mudpi.thread_events["mudpi_running"].is_set(): - if self.mudpi.thread_events["core_running"].is_set(): - if self.config["thread_events"]["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.config["thread_events"]["camera_available"].set() - else: - time.sleep(2) - return - - def work(self): - self.reset_elapsed_time() - - while self.mudpi.thread_events["mudpi_running"].is_set(): - if self.mudpi.thread_events["core_running"].is_set(): - - if self.config["thread_events"]["camera_available"].is_set(): - # try: - for i, filename in enumerate( - self.camera.capture_continuous( - self.path + 'mudpi-{counter:05d}.jpg')): - - if not self.config["thread_events"]["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 - Logger.log(LOG_LEVEL["info"], - f"{f'Camera Worker [{self.key}]...':.<{FONT_PADDING}} {FONT_YELLOW}Stopping{FONT_RESET}") - self.camera.close() - self.listener.join() - self.pubsub.close() - Logger.log(LOG_LEVEL["warning"], - f"{f'Camera Worker [{self.key}]...':.<{FONT_PADDING}} {FONT_RED}Shutdown{FONT_RESET}") diff --git a/build/lib/mudpi/workers/linux/control_worker.py b/build/lib/mudpi/workers/linux/control_worker.py deleted file mode 100644 index eb2c772..0000000 --- a/build/lib/mudpi/workers/linux/control_worker.py +++ /dev/null @@ -1,79 +0,0 @@ -import sys -import time - -from mudpi.workers.linux.worker import Worker -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.controls.linux.button_control import (ButtonControl) -from mudpi.controls.linux.switch_control import (SwitchControl) - - -class LinuxControlWorker(Worker): - def __init__(self, config, main_thread_running, system_ready): - super().__init__(config, main_thread_running, system_ready) - self.topic = self.config.get('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/build/lib/mudpi/workers/linux/display_worker.py b/build/lib/mudpi/workers/linux/display_worker.py deleted file mode 100644 index d4d2b72..0000000 --- a/build/lib/mudpi/workers/linux/display_worker.py +++ /dev/null @@ -1,261 +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 mudpi.workers.worker import Worker - -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class DisplayWorker(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/build/lib/mudpi/workers/linux/i2c_worker.py b/build/lib/mudpi/workers/linux/i2c_worker.py deleted file mode 100644 index 564fdd1..0000000 --- a/build/lib/mudpi/workers/linux/i2c_worker.py +++ /dev/null @@ -1,88 +0,0 @@ -import sys -import time -import json - -from mudpi.workers.worker import Worker -from mudpi.sensors.linux.i2c.t9602_sensor import (T9602Sensor) -from mudpi.sensors.linux.i2c.bme680_sensor import (Bme680Sensor) - -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class LinuxI2CWorker(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.linux.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/build/lib/mudpi/workers/linux/relay_worker.py b/build/lib/mudpi/workers/linux/relay_worker.py deleted file mode 100644 index 03c7c61..0000000 --- a/build/lib/mudpi/workers/linux/relay_worker.py +++ /dev/null @@ -1,216 +0,0 @@ -import sys -import json -import time -import redis -import threading -import digitalio - -from mudpi.workers import Worker - -from mudpi.logger.Logger import Logger, LOG_LEVEL -from mudpi.constants import __version__, PATH_CONFIG, DEFAULT_CONFIG_FILE, FONT_RESET_CURSOR, FONT_RESET, YELLOW_BACK, GREEN_BACK, FONT_GREEN, FONT_RED, FONT_YELLOW, FONT_PADDING - -try: - import board - SUPPORTED_DEVICE = True -except: - SUPPORTED_DEVICE = False - - -class RelayWorker(Worker): - def __init__(self, mudpi, config): - super().__init__(mudpi, config) - - 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'] - - - if SUPPORTED_DEVICE: - self.pin_obj = getattr(board, self.config['pin']) - - # Events - if self.config.get("thread_events"): - self.relay_available = self.config["thread_events"].get("relay_available") - self.relay_active = self.config["thread_events"].get("relay_active") - else: - self.config["thread_events"] = {} - self.relay_available = self.config["thread_events"]["relay_available"] = threading.Event() - self.relay_active = self.config["thread_events"]["relay_active"] = threading.Event() - - # 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 - - config = self.config - - # Pubsub Listeners - try: - self.r = self.config["redis"] - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - - self.pubsub = self.r.pubsub() - self.pubsub.subscribe(**{self.topic: self.handle_message}) - - self.init() - return - - def init(self): - super().init() - if SUPPORTED_DEVICE: - Logger.log( - LOG_LEVEL["info"], - f'{f"Relay Worker {self.key}":.<{FONT_PADDING}} {FONT_YELLOW}Initializing{FONT_RESET}' - ) - 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["warning"], - f'{f"Relay Worker {self.key}":.<{FONT_PADDING}} {FONT_GREEN}Working{FONT_RESET}' - ) - 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 SUPPORTED_DEVICE: - 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 SUPPORTED_DEVICE: - 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.mudpi.thread_events["mudpi_running"].is_set(): - if self.mudpi.thread_events["core_running"].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 - Logger.log(LOG_LEVEL["info"], - f"{f'Relay [{self.key}]...':.<{FONT_PADDING}} {FONT_YELLOW}Stopping{FONT_RESET}") - # Close the pubsub connection - self.pubsub.close() - Logger.log(LOG_LEVEL["warning"], - f"{f'Relay [{self.key}]...':.<{FONT_PADDING}} {FONT_RED}Shutdown{FONT_RESET}") diff --git a/build/lib/mudpi/workers/linux/sensor_worker.py b/build/lib/mudpi/workers/linux/sensor_worker.py deleted file mode 100644 index a4f35d2..0000000 --- a/build/lib/mudpi/workers/linux/sensor_worker.py +++ /dev/null @@ -1,100 +0,0 @@ -import sys -import time -import json - -from mudpi.workers import Worker - -from mudpi.sensors.linux.float_sensor import (FloatSensor) -from mudpi.sensors.linux.humidity_sensor import (HumiditySensor) - -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class LinuxSensorWorker(Worker): - def __init__(self, mudpi, config): - super().__init__(mudpi, config) - self.topic = config.get('topic', 'sensors').replace(" ", "_").lower() - self.sleep_duration = config.get('sleep_duration', 30) - - self.sensors = [] - self.init() - return - - def init(self): - super().init() - 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.mudpi.thread_events["mudpi_running"].is_set(): - if self.mudpi.thread_events["core_running"].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/build/lib/mudpi/workers/linux/worker.py b/build/lib/mudpi/workers/linux/worker.py deleted file mode 100644 index 7a7f771..0000000 --- a/build/lib/mudpi/workers/linux/worker.py +++ /dev/null @@ -1,87 +0,0 @@ -import sys -import json -import time -import redis -import threading - -from mudpi.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: - # {folder}.{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/build/lib/mudpi/workers/sequence_worker.py b/build/lib/mudpi/workers/sequence_worker.py deleted file mode 100644 index 8a97baf..0000000 --- a/build/lib/mudpi/workers/sequence_worker.py +++ /dev/null @@ -1,360 +0,0 @@ -import time -import json -import redis -import threading -import datetime -import importlib - -from mudpi.constants import FONT_RESET, YELLOW_BACK, GREEN_BACK, FONT_GREEN, FONT_RED, FONT_YELLOW, FONT_PADDING -from mudpi.workers import Worker -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -class SequenceWorker(Worker): - def __init__(self, mudpi, config): - super().__init__(mudpi, config) - - 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', '') - - self.actions = mudpi.actions - - self.sequence = self.config['sequence'] if self.config[ - 'sequence'] is not None else [] - - 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 - if self.config.get("thread_events"): - self.sequence_available = self.config["thread_events"].get("sequence_available") - self.sequence_active = self.config["thread_events"].get("sequence_active") - else: - self.config["thread_events"] = {} - self.sequence_available = self.config["thread_events"]["sequence_available"] = threading.Event() - self.sequence_active = self.config["thread_events"]["sequence_active"] = threading.Event() - - # PubSub - try: - self.r = self.config["redis"] - except KeyError: - self.r = redis.Redis(host='127.0.0.1', port=6379) - - # Pubsub Listeners - self.pubsub = self.r.pubsub() - - # Persit changes to config arg for - config = self.config - - self.reset_elapsed_time() - self.init() - return - - def init(self): - super().init() - self.pubsub.subscribe(**{self.topic: self.handle_message}) - return - - 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 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"], - f'Sequence {self.name} Started{FONT_RESET}' - ) - - 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"], - f'Sequence {self.name} Stopped{FONT_RESET}' - ) - - 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 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 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"], - f'Sequence {self.name} Next Step Triggered{FONT_RESET}' - ) - elif decoded_message['event'] == 'SequencePreviousStep': - self.previous_step() - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Previous Step Triggered{FONT_RESET}' - ) - elif decoded_message['event'] == 'SequenceStart': - self.start() - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Start Triggered{FONT_RESET}' - ) - elif decoded_message['event'] == 'SequenceSkipStep': - self.step_complete = True - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Skip Step Triggered{FONT_RESET}' - ) - elif decoded_message['event'] == 'SequenceStop': - self.stop() - Logger.log( - LOG_LEVEL["info"], - f'Sequence {self.name} Stop Triggered{FONT_RESET}' - ) - except: - Logger.log( - LOG_LEVEL["info"], - f"Error Decoding Message for Sequence {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"], - f"Error triggering sequence action {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 wait(self, duration=0): - time_remaining = duration - while time_remaining > 0 and self.mudpi.thread_events["mudpi_running"].is_set(): - self.pubsub.get_message() - time.sleep(1) - time_remaining -= 1 - - def run(self): - Logger.log(LOG_LEVEL["warning"], - f"{f'Sequence [{self.name}]':.<{FONT_PADDING}} {FONT_GREEN}Working{FONT_RESET}") - return super().run() - - def work(self): - self.reset_elapsed_time() - while self.mudpi.thread_events["mudpi_running"].is_set(): - if self.mudpi.thread_events["core_running"].is_set(): - try: - self.pubsub.get_message() - if self.sequence_available.is_set(): - if self.sequence_active.is_set(): - while not self.step_complete and self.mudpi.thread_events["mudpi_running"].is_set(): - 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"], - f"{f'Sequence Worker {self.key}':.<{FONT_PADDING}} {FONT_RED}Unexpected Error{FONT_RESET}") - Logger.log(LOG_LEVEL["critical"], e) - time.sleep(3) - 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 - Logger.log(LOG_LEVEL["info"], - f"{f'Sequence [{self.key}]...':.<{FONT_PADDING}} {FONT_YELLOW}Stopping{FONT_RESET}") - # Close the pubsub connection - self.pubsub.close() - Logger.log(LOG_LEVEL["warning"], - f"{f'Sequence [{self.key}]...':.<{FONT_PADDING}} {FONT_RED}Shutdown{FONT_RESET}") diff --git a/build/lib/mudpi/workers/worker.py b/build/lib/mudpi/workers/worker.py deleted file mode 100644 index 143b5f0..0000000 --- a/build/lib/mudpi/workers/worker.py +++ /dev/null @@ -1,79 +0,0 @@ -import sys -import time -import json -import redis -import datetime -import threading - -from mudpi import constants -from mudpi.logger.Logger import Logger, LOG_LEVEL - - -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 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 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: - # print('Json Error. Str Found') - return {'event': 'Unknown', 'data': message} - else: - # print('Failed to detect type') - return {'event': 'Unknown', 'data': message} From 7aa887c5004251406479e96f87a46df561d6bd24 Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Thu, 18 Mar 2021 14:42:56 +0100 Subject: [PATCH 6/9] Comment the build folder so it can get deleted --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5391d87..fcb5221 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ __pycache__/ # Distribution / packaging .Python -build/ +#build/ develop-eggs/ dist/ downloads/ From 521ab7021aaff0e897d94ba0d9d97a3935dc02aa Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Thu, 18 Mar 2021 14:50:19 +0100 Subject: [PATCH 7/9] Add validation for not existing pin on a given board. --- .gitignore | 2 +- mudpi/extensions/dht/sensor.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index fcb5221..5391d87 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ __pycache__/ # Distribution / packaging .Python -#build/ +build/ develop-eggs/ dist/ downloads/ diff --git a/mudpi/extensions/dht/sensor.py b/mudpi/extensions/dht/sensor.py index 3a09813..4ed0790 100644 --- a/mudpi/extensions/dht/sensor.py +++ b/mudpi/extensions/dht/sensor.py @@ -68,7 +68,14 @@ def classifier(self): def init(self): """ Connect to the device """ - self.pin_obj = getattr(board, self.config['pin']) + try: + self.pin_obj = getattr(board, self.config['pin']) + except AttributeError: + raise ConfigError( + "Seem like the pin does not exists" + "https://github.com/adafruit/Adafruit_Blinka/tree/master/src/adafruit_blinka/board" + ) + self.type = self.config['model'] sensor_types = { From 996ba048679efc6873a833e171a0f390256b5bb8 Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Thu, 18 Mar 2021 15:19:35 +0100 Subject: [PATCH 8/9] Resolve some linting issues --- examples/custom_extension/grow/__init__.py | 41 ++++----- mudpi/config.py | 37 ++++---- mudpi/events/adaptors/__init__.py | 100 ++++++++++----------- mudpi/managers/core_manager.py | 3 +- mudpi/managers/extension_manager.py | 1 - mudpi/sensors/arduino/soil_sensor.py | 6 +- mudpi/server/mudpi_server.py | 2 +- 7 files changed, 97 insertions(+), 93 deletions(-) diff --git a/examples/custom_extension/grow/__init__.py b/examples/custom_extension/grow/__init__.py index 6706ab4..4dbe865 100644 --- a/examples/custom_extension/grow/__init__.py +++ b/examples/custom_extension/grow/__init__.py @@ -1,36 +1,37 @@ """ - Custom Extension Example - Provide a good description of what - your extension is adding to MudPi - or what it does. + Custom Extension Example + Provide a good description of what + your extension is adding to MudPi + or what it does. """ from mudpi.extensions import BaseExtension + # Your extension should extend the BaseExtension class class Extension(BaseExtension): # The minimum your extension needs is a namespace. This # should be the same as your folder name and unique for # all extensions. Interfaces all components use this namespace. - namespace = 'grow' + namespace = 'grow' + + # You can also set an update interval at which components + # should be updated to gather new data / state. + update_interval = 1 - # You can also set an update interval at which components - # should be updated to gather new data / state. - update_interval = 1 - - def init(self, config): - """ Prepare the extension and all components """ - # This is called on MudPi start and passed config on start. - # Here is where devices should be setup, connections made, - # components created and added etc. - - # Must return True or an error will be assumed disabling the extension - return True + def init(self, config): + """ Prepare the extension and all components """ + # This is called on MudPi start and passed config on start. + # Here is where devices should be setup, connections made, + # components created and added etc. + + # Must return True or an error will be assumed disabling the extension + return True def validate(self, config): """ Validate the extension configuration """ # Here the extension configuration is passed in before the init() method - # is called. The validate method is used to prepare a valid configuration + # is called. The validate method is used to prepare a valid configuration # for the extension before initialization. This method should return the # validated config or raise a ConfigError. - - return config \ No newline at end of file + + return config diff --git a/mudpi/config.py b/mudpi/config.py index a3fcc57..a10080d 100644 --- a/mudpi/config.py +++ b/mudpi/config.py @@ -1,8 +1,9 @@ import os import json import yaml -from mudpi.constants import (FONT_YELLOW, RED_BACK, FONT_RESET, IMPERIAL_SYSTEM, PATH_MUDPI, PATH_CONFIG, DEFAULT_CONFIG_FILE) -from mudpi.exceptions import ConfigNotFoundError, ConfigError +from mudpi.constants import (FONT_YELLOW, RED_BACK, FONT_RESET, IMPERIAL_SYSTEM, PATH_MUDPI, + PATH_CONFIG, DEFAULT_CONFIG_FILE) +from mudpi.exceptions import ConfigNotFoundError, ConfigError, ConfigFormatError class Config(object): @@ -11,14 +12,15 @@ class Config(object): A class to represent the MudPi configuration that is typically pulled from a file. """ + def __init__(self, config_path=None): self.config_path = config_path or os.path.abspath(os.path.join(os.getcwd(), PATH_CONFIG)) - + self.config = {} self.set_defaults() - """ Properties """ + @property def name(self): return self.config.get('mudpi', {}).get('name', 'MudPi') @@ -35,8 +37,8 @@ def debug(self): def debug(self, value): self.config.setdefault('mudpi', {})['debug'] = value - """ Methods """ + def path(self, *path): """ Returns path relative to the config folder. """ return os.path.join(self.config_path, *path) @@ -122,7 +124,8 @@ def load_from_json(self, json_data): self.config = json.loads(json_data) return self.config except Exception as e: - print(f'{RED_BACK}Problem loading configs from JSON {FONT_RESET}\n{FONT_YELLOW}{e}{FONT_RESET}\r') + print( + f'{RED_BACK}Problem loading configs from JSON {FONT_RESET}\n{FONT_YELLOW}{e}{FONT_RESET}\r') def load_from_yaml(self, yaml_data): """ Load configs from YAML """ @@ -130,7 +133,8 @@ def load_from_yaml(self, yaml_data): self.config = yaml.load(yaml_data, yaml.FullLoader) return self.config except Exception as e: - print(f'{RED_BACK}Problem loading configs from YAML {FONT_RESET}\n{FONT_YELLOW}{e}{FONT_RESET}\r') + print( + f'{RED_BACK}Problem loading configs from YAML {FONT_RESET}\n{FONT_YELLOW}{e}{FONT_RESET}\r') def save_to_file(self, file=None, format=None, config=None): """ Save current configs to a file @@ -159,14 +163,15 @@ def save_to_file(self, file=None, format=None, config=None): def validate_file(self, file): """ Validate a file path and return a prepared path to save """ - if '.' in file: + if '.' in file: if not self.file_exists(file): raise ConfigNotFoundError(f"The config path {file} does not exist.") - return False + extensions = ['.config', '.json', '.yaml', '.conf'] + if not any([file.endswith(extension) for extension in extensions]): - raise ConfigFormatError("An unknown config file format was provided in the config path.") - return False + raise ConfigFormatError( + "An unknown config file format was provided in the config path.") else: # Path provided but not file file = os.path.join(file, DEFAULT_CONFIG_FILE) @@ -174,15 +179,15 @@ def validate_file(self, file): def config_format(self, file): """ Returns the file format if supported """ + + config_format = None if '.' in file: if any(extension in file for extension in ['.config', '.json', '.conf']): config_format = 'json' elif '.yaml' in file: config_format = 'yaml' - else: - config_format = None - - return config_format + + return config_format def get(self, key, default=None, replace_char=None): """ Get an item from the config with a default @@ -198,4 +203,4 @@ def get(self, key, default=None, replace_char=None): def __repr__(self): """ Debug print of config """ - return f'' \ No newline at end of file + return f'' diff --git a/mudpi/events/adaptors/__init__.py b/mudpi/events/adaptors/__init__.py index be3d783..cf015b3 100644 --- a/mudpi/events/adaptors/__init__.py +++ b/mudpi/events/adaptors/__init__.py @@ -1,54 +1,54 @@ class Adaptor: - """ Base adaptor for pubsub event system """ - - # This key should represent key in configs that it will load form - key = None - - adaptors = {} - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - cls.adaptors[cls.key] = cls - - def __init__(self, config={}): - self.config = config - - def connect(self): - """ Authenticate to system and cache connections """ - raise NotImplementedError() - - def disconnect(self): - """ Close active connections and cleanup subscribers """ - raise NotImplementedError() - - def subscribe(self, topic, callback): - """ Listen on a topic and pass event data to callback """ - raise NotImplementedError() - - def unsubscribe(self, topic): - """ Stop listening for events on a topic """ - raise NotImplementedError() - - def publish(self, topic, data=None): - """ Publish an event on the topic """ - raise NotImplementedError() - - """ No need to override this unless necessary """ - def subscribe_once(self, topic, callback): - """ Subscribe to topic for only one event """ - def handle_once(data): - """ Wrapper to unsubscribe after event handled """ - self.unsubscribe(topic) - if callable(callback): - # Pass data to real callback - callback(data) - - return self.subscribe(topic, handle_once) - - def get_message(self): - """ Some protocols need to initate a poll for new messages """ - pass - + """ Base adaptor for pubsub event system """ + + # This key should represent key in configs that it will load form + key = None + + adaptors = {} + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls.adaptors[cls.key] = cls + + def __init__(self, config={}): + self.config = config + + def connect(self): + """ Authenticate to system and cache connections """ + raise NotImplementedError() + + def disconnect(self): + """ Close active connections and cleanup subscribers """ + raise NotImplementedError() + + def subscribe(self, topic, callback): + """ Listen on a topic and pass event data to callback """ + raise NotImplementedError() + + def unsubscribe(self, topic): + """ Stop listening for events on a topic """ + raise NotImplementedError() + + def publish(self, topic, data=None): + """ Publish an event on the topic """ + raise NotImplementedError() + + """ No need to override this unless necessary """ + def subscribe_once(self, topic, callback): + """ Subscribe to topic for only one event """ + def handle_once(data): + """ Wrapper to unsubscribe after event handled """ + self.unsubscribe(topic) + if callable(callback): + # Pass data to real callback + callback(data) + + return self.subscribe(topic, handle_once) + + def get_message(self): + """ Some protocols need to initate a poll for new messages """ + pass + # Import adaptors from . import redis, mqtt diff --git a/mudpi/managers/core_manager.py b/mudpi/managers/core_manager.py index 268046d..6eaf534 100644 --- a/mudpi/managers/core_manager.py +++ b/mudpi/managers/core_manager.py @@ -184,8 +184,7 @@ def validate_config(self, config_path): """ Validate that config path was provided and a file """ if not os.path.exists(config_path): raise ConfigNotFoundError(f"Config File Doesn't Exist at {config_path}") - return False - else: + else: # No config file provided just a path pass diff --git a/mudpi/managers/extension_manager.py b/mudpi/managers/extension_manager.py index 9170ede..e5913e6 100644 --- a/mudpi/managers/extension_manager.py +++ b/mudpi/managers/extension_manager.py @@ -124,7 +124,6 @@ def find_or_create_interface(self, interface_name, interface_config = {}): if self.config is None: raise MudPiError("Config was null in extension manager. Call `init(config)` first.") - return # Get the interface and extension interface, extension = self.importer.prepare_interface_and_import(interface_name) diff --git a/mudpi/sensors/arduino/soil_sensor.py b/mudpi/sensors/arduino/soil_sensor.py index 414ef88..30e8f6d 100644 --- a/mudpi/sensors/arduino/soil_sensor.py +++ b/mudpi/sensors/arduino/soil_sensor.py @@ -15,9 +15,9 @@ # Wet Water = 287 # Dry Air = 584 -AirBounds = 590; -WaterBounds = 280; -intervals = int((AirBounds - WaterBounds) / 3); +AirBounds = 590 +WaterBounds = 280 +intervals = int((AirBounds - WaterBounds) / 3) class SoilSensor(Sensor): diff --git a/mudpi/server/mudpi_server.py b/mudpi/server/mudpi_server.py index 9f472fb..d52e8c5 100644 --- a/mudpi/server/mudpi_server.py +++ b/mudpi/server/mudpi_server.py @@ -106,7 +106,7 @@ def decodeMessageData(self, message): system_ready = threading.Event() system_ready.set() server = MudpiServer(config, system_ready) - server.listen(); + server.listen() try: while system_ready.is_set(): time.sleep(1) From 10bc2288d9b4a4c4f5551589822d1f68c1cb9800 Mon Sep 17 00:00:00 2001 From: yeyeto2788 Date: Mon, 22 Mar 2021 09:14:14 +0100 Subject: [PATCH 9/9] Revert little change on logger --- .gitignore | 2 ++ mudpi/logger/Logger.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f3b5400..6a24210 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,5 @@ venv.bak/ # Rope project settings .ropeproject + +.idea \ No newline at end of file diff --git a/mudpi/logger/Logger.py b/mudpi/logger/Logger.py index e361206..eb070c9 100644 --- a/mudpi/logger/Logger.py +++ b/mudpi/logger/Logger.py @@ -106,7 +106,7 @@ def log(log_level, msg: str): # for ease of access from outside log_level = LOG_LEVEL[log_level] else: log_level = LOG_LEVEL['unknown'] - Logger.logger.log(log_level, msg) + Logger.logger.log_this(log_level, msg) @staticmethod def log_formatted(log_level, message, status='', status_level=None, padding=FONT_PADDING, spacer="."):