├── mudpi ├── __init__.py ├── logger │ └── __init__.py ├── managers │ └── __init__.py ├── sensors │ ├── __init__.py │ ├── mcp3xxx │ │ ├── __init__.py │ │ ├── sensor.py │ │ └── soil_sensor.py │ ├── base_sensor.py │ └── arduino │ │ └── soil_sensor.py ├── __init__.pyc ├── extensions │ ├── timer │ │ ├── extension.json │ │ ├── __init__.py │ │ └── sensor.py │ ├── example │ │ ├── extension.json │ │ ├── __init__.py │ │ ├── sensor.py │ │ ├── char_display.py │ │ ├── toggle.py │ │ └── control.py │ ├── camera │ │ └── extension.json │ ├── action │ │ ├── extension.json │ │ └── __init__.py │ ├── group │ │ ├── extension.json │ │ ├── __init__.py │ │ └── trigger.py │ ├── mqtt │ │ ├── extension.json │ │ ├── __init__.py │ │ └── sensor.py │ ├── redis │ │ ├── extension.json │ │ └── __init__.py │ ├── nfc │ │ ├── extension.json │ │ └── trigger.py │ ├── cron │ │ ├── extension.json │ │ ├── __init__.py │ │ └── trigger.py │ ├── state │ │ ├── extension.json │ │ ├── __init__.py │ │ └── trigger.py │ ├── socket │ │ └── extension.json │ ├── toggle │ │ ├── extension.json │ │ └── trigger.py │ ├── rtsp │ │ ├── extension.json │ │ └── __init__.py │ ├── sensor │ │ ├── extension.json │ │ ├── __init__.py │ │ └── trigger.py │ ├── sun │ │ ├── extension.json │ │ ├── __init__.py │ │ └── trigger.py │ ├── trigger │ │ └── extension.json │ ├── control │ │ ├── extension.json │ │ ├── __init__.py │ │ └── trigger.py │ ├── picamera │ │ ├── extension.json │ │ ├── __init__.py │ │ └── camera.py │ ├── sequence │ │ └── extension.json │ ├── char_display │ │ └── extension.json │ ├── gpio │ │ ├── extension.json │ │ ├── __init__.py │ │ ├── sensor.py │ │ └── toggle.py │ ├── i2c │ │ ├── __init__.py │ │ ├── extension.json │ │ ├── char_display.py │ │ └── toggle.py │ ├── nanpy │ │ └── extension.json │ ├── t9602 │ │ ├── extension.json │ │ ├── __init__.py │ │ └── sensor.py │ ├── bme280 │ │ ├── __init__.py │ │ ├── extension.json │ │ └── sensor.py │ ├── bme680 │ │ ├── __init__.py │ │ ├── extension.json │ │ └── sensor.py │ ├── dht │ │ ├── extension.json │ │ ├── __init__.py │ │ └── sensor.py │ └── dht_legacy │ │ ├── extension.json │ │ ├── __init__.py │ │ └── sensor.py ├── debug │ ├── blink.py │ ├── firmware_test.py │ └── dump.py ├── tools │ ├── add_lcd_message.py │ ├── event_send_tool.py │ ├── lcd_message_tool.py │ └── lcd_reset.py ├── mudpi.config.example ├── exceptions.py ├── events │ ├── adaptors │ │ ├── __init__.py │ │ ├── redis.py │ │ └── mqtt.py │ └── __init__.py ├── utils.py ├── constants.py ├── server │ ├── mudpi_server.py │ └── mudpi_event_listener.js └── workers │ ├── __init__.py │ └── adc_worker.py ├── requirements.txt ├── MANIFEST.in ├── examples ├── custom_extension │ ├── mudpi.json │ └── grow │ │ ├── extension.json │ │ ├── __init__.py │ │ └── trigger.py ├── cron_trigger │ └── mudpi.json ├── example_sensor │ └── mudpi.json ├── custom_actions │ └── mudpi.json ├── sun │ └── mudpi.json ├── nanpy_node │ └── mudpi.json ├── timer │ └── mudpi.json ├── nfc │ └── mudpi.json └── automation_sequence │ └── mudpi.json ├── setup.py ├── .github ├── ISSUE_TEMPLATE │ └── bug-report.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore └── README.md /mudpi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mudpi/logger/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mudpi/managers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mudpi/sensors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mudpi/sensors/mcp3xxx/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | redis 2 | paho-mqtt 3 | pyyaml 4 | -------------------------------------------------------------------------------- /mudpi/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudpi/mudpi-core/HEAD/mudpi/__init__.pyc -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include *.json 3 | include *.yaml 4 | include *.yml 5 | include *.config 6 | include *.config.example 7 | include *.sh -------------------------------------------------------------------------------- /mudpi/extensions/timer/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Timer", 3 | "namespace": "timer", 4 | "details": { 5 | "description": "Timer components to track elapsed time." 6 | }, 7 | "requirements": [] 8 | } -------------------------------------------------------------------------------- /mudpi/extensions/example/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example", 3 | "namespace": "example", 4 | "details": { 5 | "description": "Example components for testing and demontstration." 6 | }, 7 | "requirements": [] 8 | } -------------------------------------------------------------------------------- /examples/custom_extension/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Custom Extension Example", 4 | "debug": false, 5 | }, 6 | "grow": [ 7 | { 8 | "hello":"world", 9 | "test": 123 10 | } 11 | ]} -------------------------------------------------------------------------------- /mudpi/extensions/camera/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Camera", 3 | "namespace": "camera", 4 | "details": { 5 | "description": "Camera photo capture and streaming.", 6 | "documentation": "https://mudpi.app/docs/camera" 7 | } 8 | } -------------------------------------------------------------------------------- /mudpi/extensions/action/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Action", 3 | "namespace": "action", 4 | "details": { 5 | "description": "Custom behavior through configurations.", 6 | "documentation": "https://mudpi.app/docs/actions" 7 | } 8 | } -------------------------------------------------------------------------------- /mudpi/extensions/group/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Groups", 3 | "namespace": "group", 4 | "details": { 5 | "description": "Enables grouping for components (Triggers).", 6 | "documentation": "https://mudpi.app/docs/triggers" 7 | } 8 | } -------------------------------------------------------------------------------- /mudpi/extensions/mqtt/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MQTT", 3 | "namespace": "mqtt", 4 | "details": { 5 | "description": "MQTT support for components.", 6 | "documentation": "https://mudpi.app/docs/mqtt" 7 | }, 8 | "requirements": ["paho-mqtt"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/redis/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Redis", 3 | "namespace": "redis", 4 | "details": { 5 | "description": "Redis support for components.", 6 | "documentation": "https://mudpi.app/docs/redis" 7 | }, 8 | "requirements": ["redis"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/nfc/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NFC", 3 | "namespace": "nfc", 4 | "details": { 5 | "description": "NFC tag and card reading support.", 6 | "documentation": "https://mudpi.app/docs/extensions-nfc" 7 | }, 8 | "requirements": ["nfcpy"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/cron/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cron", 3 | "namespace": "cron", 4 | "details": { 5 | "description": "Cron job schedule support for components.", 6 | "documentation": "https://mudpi.app/docs/triggers" 7 | }, 8 | "requirements": ["pycron"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/state/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "State Manager", 3 | "namespace": "state", 4 | "details": { 5 | "description": "Interfaces to core Mudpi state manager", 6 | "documentation": "https://mudpi.app/docs/states" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/group/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Group Extension 3 | Allows grouping of components. 4 | """ 5 | from mudpi.extensions import BaseExtension 6 | 7 | 8 | class Extension(BaseExtension): 9 | namespace = 'group' 10 | update_interval = 0.2 11 | -------------------------------------------------------------------------------- /mudpi/extensions/socket/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Socket Server", 3 | "namespace": "socket", 4 | "details": { 5 | "description": "Simple socket server to interface with MudPi", 6 | "documentation": "https://mudpi.app/docs/socket" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/toggle/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Toggle", 3 | "namespace": "toggle", 4 | "details": { 5 | "description": "Interfaces with components that can turn on and off.", 6 | "documentation": "https://mudpi.app/docs/toggle" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/rtsp/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RTSP Stream", 3 | "namespace": "rtsp", 4 | "details": { 5 | "description": "Capture images and videos from rtsp / http.", 6 | "documentation": "https://mudpi.app/docs/rtsp" 7 | }, 8 | "requirements": ["opencv-python"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/sensor/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sensors", 3 | "namespace": "sensor", 4 | "details": { 5 | "description": "Interfaces with components to get data periodically.", 6 | "documentation": "https://mudpi.app/docs/sensors" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/sun/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sun Schedule", 3 | "namespace": "sun", 4 | "details": { 5 | "description": "Sun tracking for sunrise and sunset", 6 | "documentation": "https://mudpi.app/docs/extensions/sun" 7 | }, 8 | "requirements": ["requests"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/trigger/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Triggers", 3 | "namespace": "trigger", 4 | "details": { 5 | "description": "Causes actions based on events or state changes.", 6 | "documentation": "https://mudpi.app/docs/triggers" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/control/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Controls", 3 | "namespace": "control", 4 | "details": { 5 | "description": "Take physical input from user such as button, switch, etc.", 6 | "documentation": "https://mudpi.app/docs/controls" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/picamera/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Raspberry Pi Camera", 3 | "namespace": "picamera", 4 | "details": { 5 | "description": "Camera module for Raspberry Pi", 6 | "documentation": "https://mudpi.app/docs/camera/picamera" 7 | }, 8 | "requirements": ["picamera"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/sequence/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Automation Sequences", 3 | "namespace": "sequence", 4 | "details": { 5 | "description": "Fire multiple actions in sequential order.", 6 | "documentation": "https://mudpi.app/docs/sequences" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/char_display/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LCD Character Displays", 3 | "namespace": "char_display", 4 | "details": { 5 | "description": "Display messages through LCD displays.", 6 | "documentation": "https://mudpi.app/docs/lcd_displays" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /examples/custom_extension/grow/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Grow - A Friendly Display Name", 3 | "namespace": "grow", 4 | "details": { 5 | "description": "Grow - a few words about the extension", 6 | "documentation": "https://mudpi.app/docs/extending-mudpi" 7 | }, 8 | "requirements": [] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/cron/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cron Extension 3 | Cron schedule support for triggers 4 | to allow scheduling. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'cron' 11 | update_interval = 1 12 | -------------------------------------------------------------------------------- /mudpi/extensions/gpio/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GPIO (Linux)", 3 | "namespace": "gpio", 4 | "details": { 5 | "description": "Interfaces using linux board GPIO.", 6 | "documentation": "https://mudpi.app/docs/gpio" 7 | }, 8 | "requirements": ["adafruit-blinka", "adafruit-circuitpython-debouncer"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/i2c/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | I2C Extension 3 | Supports I2C protocol and 4 | provides interfaces for components. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'i2c' 11 | update_interval = 0.5 12 | -------------------------------------------------------------------------------- /mudpi/extensions/state/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | State Extension 3 | Gives support to interfaces with 4 | the MudPi internal State Manager. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'state' 11 | update_interval = 0.2 12 | -------------------------------------------------------------------------------- /mudpi/extensions/nanpy/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nanpy", 3 | "namespace": "nanpy", 4 | "details": { 5 | "description": "Control arduino and ESP boards through serial/wifi.", 6 | "documentation": "https://mudpi.app/docs/nodes" 7 | }, 8 | "requirements": ["https://github.com/olixr/nanpy/archive/master.zip"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/t9602/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "T9602 I2C Environment Sensor", 3 | "namespace": "t9602", 4 | "details": { 5 | "description": "Provides interface for T9602 sensors to take environment readings.", 6 | "documentation": "https://mudpi.app/docs/sensors/T9602" 7 | }, 8 | "requirements": ["smbus"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/bme280/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | BME280 Extension 3 | Includes sensor interface for BME280. 4 | Works on i2c over linux boards. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'bme280' 11 | update_interval = 30 12 | 13 | -------------------------------------------------------------------------------- /mudpi/extensions/bme680/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | BME680 Extension 3 | Includes sensor interface for BME680. 4 | Works on i2c over linux boards. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'bme680' 11 | update_interval = 30 12 | 13 | -------------------------------------------------------------------------------- /mudpi/extensions/gpio/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | GPIO Extension 3 | Includes interfaces for linux board 4 | GPIO. Supports many linux based boards. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'gpio' 11 | update_interval = 30 12 | 13 | -------------------------------------------------------------------------------- /mudpi/extensions/picamera/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Raspberry Pi Camera Extension 3 | Connect to a raspberry pi camera 4 | through the picamera library. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'picamera' 11 | update_interval = 1 12 | -------------------------------------------------------------------------------- /mudpi/extensions/example/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Extension 3 | Includes some example configs for testing 4 | and has interfaces for core components. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'example' 11 | update_interval = 30 12 | 13 | -------------------------------------------------------------------------------- /mudpi/extensions/rtsp/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | RTSP Camera Extension 3 | Connects to a RTSP or HTTP camera 4 | stream to capture images and record 5 | videos. 6 | """ 7 | from mudpi.extensions import BaseExtension 8 | 9 | 10 | class Extension(BaseExtension): 11 | namespace = 'rtsp' 12 | update_interval = 1 13 | -------------------------------------------------------------------------------- /mudpi/extensions/timer/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Timer Extension 3 | Provides a sensor and trigger 4 | to do elapsed time operations. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | NAMESPACE = 'timer' 9 | 10 | class Extension(BaseExtension): 11 | namespace = NAMESPACE 12 | update_interval = 1 13 | 14 | -------------------------------------------------------------------------------- /mudpi/extensions/t9602/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | T9602 Extension 3 | Includes sensor interface for T9602. 4 | Works on i2c over linux boards (typically 5 | on a raspberry pi.) 6 | """ 7 | from mudpi.extensions import BaseExtension 8 | 9 | 10 | class Extension(BaseExtension): 11 | namespace = 't9602' 12 | update_interval = 30 13 | 14 | -------------------------------------------------------------------------------- /mudpi/extensions/bme280/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BME280 Environment Sensor", 3 | "namespace": "bme280", 4 | "details": { 5 | "description": "Provides interface for BME280 sensors to take environment readings.", 6 | "documentation": "https://mudpi.app/docs/sensors/bme280" 7 | }, 8 | "requirements": ["adafruit-blinka", "adafruit-circuitpython-bme280"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/bme680/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BME680 Environment Sensor", 3 | "namespace": "bme680", 4 | "details": { 5 | "description": "Provides interface for BME680 sensors to take environment readings.", 6 | "documentation": "https://mudpi.app/docs/sensors/bme680" 7 | }, 8 | "requirements": ["adafruit-blinka", "adafruit-circuitpython-bme680"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/dht/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Digital Humidity Temperature (DHT)", 3 | "namespace": "dht", 4 | "details": { 5 | "description": "Provides interface for DHT sensors to take climate readings.", 6 | "documentation": "https://mudpi.app/docs/pi-sensors-humidity" 7 | }, 8 | "requirements": ["adafruit-circuitpython-dht==3.5.5", "adafruit-blinka"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/dht_legacy/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Digital Humidity Temperature (DHT) [Legacy]", 3 | "namespace": "dht_legacy", 4 | "details": { 5 | "description": "Provides interface for DHT sensors to take climate readings (using depricated library).", 6 | "documentation": "https://mudpi.app/docs/extensions/dht" 7 | }, 8 | "requirements": ["Adafruit_DHT"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/dht/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | DHT Extension 3 | Includes sensor interface for DHT. 4 | Works with DHT11, DHT22, DHT2203 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | NAMESPACE = 'dht' 10 | UPDATE_INTERVAL = 30 11 | 12 | class Extension(BaseExtension): 13 | namespace = NAMESPACE 14 | update_interval = UPDATE_INTERVAL 15 | 16 | -------------------------------------------------------------------------------- /mudpi/extensions/i2c/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "I2C Components", 3 | "namespace": "i2c", 4 | "details": { 5 | "description": "Provideds I2C Support for Extensions.", 6 | "documentation": "https://mudpi.app/docs/lcd_displays" 7 | }, 8 | "requirements": ["adafruit-blinka", "git+https://github.com/olixr/Adafruit_CircuitPython_CharLCD.git#egg=adafruit-circuitpython-charlcd"] 9 | } -------------------------------------------------------------------------------- /mudpi/extensions/sun/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sun Extension 3 | Includes interfaces for getting 4 | sunrise and sunset. 5 | """ 6 | from mudpi.extensions import BaseExtension 7 | 8 | 9 | class Extension(BaseExtension): 10 | namespace = 'sun' 11 | update_interval = 60 12 | 13 | def init(self, config): 14 | """ Prepare the api connection and sun components """ 15 | self.config = config 16 | 17 | return True 18 | 19 | -------------------------------------------------------------------------------- /mudpi/extensions/dht_legacy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | DHT Extension 3 | Includes sensor interface for DHT. 4 | Works with DHT11, DHT22, DHT2203. 5 | This uses the old depricated library 6 | that works better on older boards. 7 | """ 8 | from mudpi.extensions import BaseExtension 9 | 10 | 11 | NAMESPACE = 'dht_legacy' 12 | UPDATE_INTERVAL = 30 13 | 14 | class Extension(BaseExtension): 15 | namespace = NAMESPACE 16 | update_interval = UPDATE_INTERVAL 17 | 18 | -------------------------------------------------------------------------------- /mudpi/debug/blink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Author: Andrea Stagi 4 | # Description: keeps your led blinking 5 | # Dependencies: None 6 | 7 | from nanpy import (ArduinoApi, SerialManager) 8 | from time import sleep 9 | import logging 10 | logging.basicConfig(level=logging.DEBUG) 11 | 12 | connection = SerialManager(device=str(input('Enter Device Port: ')),timeout=20) 13 | a = ArduinoApi(connection=connection) 14 | 15 | a.pinMode(13, a.OUTPUT) 16 | 17 | for i in range(100): 18 | a.digitalWrite(13, (i + 1) % 2) 19 | sleep(0.2) -------------------------------------------------------------------------------- /mudpi/tools/add_lcd_message.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import json 3 | 4 | print('Add a Message to the Queue') 5 | 6 | r = redis.Redis(host='127.0.0.1', port=6379) 7 | line1 = str(input('Line 1 Text: ')) 8 | line2 = str(input('Line 2 Text: ')) 9 | old_messages = r.get('lcdmessages') 10 | messages = [] 11 | if old_messages: 12 | messages = json.loads(old_messages.decode('utf-8')) 13 | testmessage = {'line_1': line1, 'line_2': line2} 14 | 15 | messages.append(testmessage) 16 | 17 | r.set('lcdmessages', json.dumps(messages)) 18 | 19 | print('Value set in redis and in queue') 20 | -------------------------------------------------------------------------------- /examples/cron_trigger/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Cron Example", 4 | "debug": false, 5 | "location": { 6 | "latitude":4, 7 | "longitude":-8.0 8 | } 9 | }, 10 | "trigger": [ 11 | { 12 | "interface": "cron", 13 | "key": "toggle_every_2_mins", 14 | "name": "Toggle Every 2 Mins", 15 | "schedule": "*/2 * * * *", 16 | "actions": [".example_pump.toggle"] 17 | } 18 | ], 19 | "toggle": [ 20 | { 21 | "key": "example_pump", 22 | "interface": "example" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /mudpi/debug/firmware_test.py: -------------------------------------------------------------------------------- 1 | from nanpy.arduinotree import ArduinoTree 2 | from nanpy.serialmanager import SerialManager 3 | 4 | 5 | def fw_check(): 6 | connection = SerialManager(device=str(input('Enter Device Port: '))) 7 | a = ArduinoTree(connection=connection) 8 | 9 | print('Firmware classes enabled in cfg.h:') 10 | print(' ' + '\n '.join(a.connection.classinfo.firmware_name_list)) 11 | 12 | d = a.define.as_dict 13 | print( 14 | '\nYour firmware was built on:\n %s %s' % 15 | (d.get('__DATE__'), d.get('__TIME__'))) 16 | 17 | 18 | if __name__ == '__main__': 19 | fw_check() -------------------------------------------------------------------------------- /examples/example_sensor/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Example Sensors", 4 | "debug": false 5 | }, 6 | "sensor": [ 7 | { 8 | "interface": "example", 9 | "update_interval": 10, 10 | "key": "example_one", 11 | "data": 3, 12 | "classifier": "light" 13 | }, 14 | { 15 | "interface": "example", 16 | "key": "example_two", 17 | "update_interval": 3, 18 | "data": 20, 19 | "classifier": "power" 20 | }, 21 | { 22 | "interface": "example", 23 | "key": "example_three", 24 | "data": 5 25 | } 26 | ]} -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | from mudpi.constants import __version__ 3 | setup( 4 | name='mudpi', 5 | version=__version__, 6 | description="Privacy Focused Automation for the Garden & Home", 7 | author="Eric Davisson", 8 | author_email="eric@mudpi.app", 9 | url="https://mudpi.app", 10 | include_package_data=True, 11 | package_data={'': ['*.json', '*.yml', '*.config', '*.config.example', '*.sh']}, 12 | packages=find_packages(exclude=['tools', 'tests', 'scripts', 'debug']), 13 | entry_points={ 14 | 'console_scripts': [ 15 | 'mudpi = mudpi.__main__:main', 16 | ], 17 | }, 18 | install_requires=[ 19 | "redis", 20 | "pyyaml", 21 | "paho-mqtt" 22 | ] 23 | ) -------------------------------------------------------------------------------- /mudpi/mudpi.config.example: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Example", 4 | "debug": false, 5 | "unit_system": "imperial", 6 | "location": { 7 | "latitude":12.526, 8 | "longitude":-33.043 9 | }, 10 | "events": { 11 | "redis": { 12 | "host": "127.0.0.1", 13 | "port": 6379 14 | } 15 | } 16 | }, 17 | "sensor": [ 18 | { 19 | "interface": "example", 20 | "update_interval": 10, 21 | "key": "example_one", 22 | "data": 3, 23 | "classifier": "light" 24 | }, 25 | { 26 | "interface": "example", 27 | "key": "example_two", 28 | "update_interval": 3, 29 | "data": 20, 30 | "classifier": "power" 31 | }, 32 | { 33 | "interface": "example", 34 | "key": "example_three", 35 | "data": 5 36 | } 37 | ]} -------------------------------------------------------------------------------- /mudpi/sensors/base_sensor.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | 4 | class BaseSensor: 5 | 6 | def __init__(self, pin, name=None, key=None, redis_conn=None): 7 | self.pin = pin 8 | 9 | if key is None: 10 | raise Exception('No "key" Found in Sensor Config') 11 | else: 12 | self.key = key.replace(" ", "_").lower() 13 | 14 | if name is None: 15 | self.name = self.key.replace("_", " ").title() 16 | else: 17 | self.name = name 18 | 19 | try: 20 | self.r = redis_conn if redis_conn is not None else redis.Redis( 21 | host='127.0.0.1', port=6379) 22 | except KeyError: 23 | self.r = redis.Redis(host='127.0.0.1', port=6379) 24 | 25 | def init_sensor(self): 26 | pass 27 | 28 | def read(self): 29 | pass 30 | 31 | def read_raw(self): 32 | pass 33 | 34 | def read_pin(self): 35 | raise NotImplementedError 36 | -------------------------------------------------------------------------------- /examples/custom_actions/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Actions Example", 4 | "debug": false, 5 | }, 6 | "action": [ 7 | { 8 | "type": "event", 9 | "name": "Display Warm Temps", 10 | "key": "display_warm_temp", 11 | "action": {"event":"Message", "data": {"message": "Warm Temperature\nCurrent: [dht_temperature]", "duration":10}}, 12 | "topic": "mudpi/lcd/1" 13 | }, 14 | { 15 | "type": "event", 16 | "name": "An Example LCD Event", 17 | "key": "example_lcd_event", 18 | "event": "Message", 19 | "data": { 20 | "message": "System Last Boot\n[started_at]", 21 | "duration":10 22 | }, 23 | "topic": "mudpi/lcd/1" 24 | }, 25 | { 26 | "type": "event", 27 | "name": "Turn Valve Off", 28 | "key": "turn_off_valve_2", 29 | "topic": "garden/pi/relays/4", 30 | "action": {"event":"Switch", "data": 0} 31 | } 32 | ]} -------------------------------------------------------------------------------- /mudpi/sensors/mcp3xxx/sensor.py: -------------------------------------------------------------------------------- 1 | import adafruit_mcp3xxx.mcp3008 as MCP 2 | 3 | # Base sensor class to extend all other mcp3xxx sensors from. 4 | from sensors.base_sensor import BaseSensor 5 | 6 | 7 | class Sensor(BaseSensor): 8 | PINS = { 9 | 0: MCP.P0, 10 | 1: MCP.P1, 11 | 2: MCP.P2, 12 | 3: MCP.P3, 13 | 4: MCP.P4, 14 | 5: MCP.P5, 15 | 6: MCP.P6, 16 | 7: MCP.P7, 17 | } 18 | 19 | def __init__(self, pin: int, mcp, name=None, key=None, redis_conn=None): 20 | super().__init__( 21 | pin=pin, 22 | name=name, 23 | key=key, 24 | redis_conn=redis_conn 25 | ) 26 | self.mcp = mcp 27 | self.topic = None 28 | 29 | def read_raw(self): 30 | """ 31 | Read the sensor(s) but return the raw voltage, useful for debugging 32 | 33 | Returns: 34 | 35 | """ 36 | return self.topic.voltage 37 | 38 | def read_pin(self): 39 | """ 40 | Read the pin from the mcp3xxx as unaltered digital value 41 | 42 | Returns: 43 | 44 | """ 45 | return self.topic.value 46 | -------------------------------------------------------------------------------- /examples/sun/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Sun Example", 4 | "debug": true, 5 | "unit_system": "imperial", 6 | "location": { 7 | "latitude": 40, 8 | "longitude": -80 9 | }, 10 | "events": { 11 | "redis": { 12 | "host": "127.0.0.1", 13 | "port": 6379 14 | }, 15 | "mqtt": { 16 | "host": "localhost" 17 | } 18 | } 19 | }, 20 | "trigger": [ 21 | { 22 | "interface": "state", 23 | "source": "sun", 24 | "nested_source": "past_sunrise", 25 | "key": "sunrise_trigger", 26 | "name": "Trigger at Sunrise", 27 | "frequency": "once", 28 | "actions": [ 29 | ".example_pump.turn_on" 30 | ], 31 | "thresholds": [ 32 | { 33 | "comparison": "eq", 34 | "value": true 35 | } 36 | ] 37 | } 38 | ], 39 | "sensor": [ 40 | { 41 | "interface": "sun", 42 | "key": "sun", 43 | "latitude": 43.0, 44 | "longitude": -88.0 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /mudpi/exceptions.py: -------------------------------------------------------------------------------- 1 | """ MudPi System Exceptions 2 | All the custom exceptions for MudPi. 3 | """ 4 | 5 | class MudPiError(Exception): 6 | """ General MudPi exception occurred. """ 7 | 8 | 9 | """ Config Errors """ 10 | class ConfigError(MudPiError): 11 | """ General error with configurations. """ 12 | 13 | class NoKeyProvidedError(ConfigError): 14 | """ When no config key is specified. """ 15 | 16 | class ConfigFormatError(ConfigError): 17 | """ Error with configuration formatting. """ 18 | 19 | class ConfigNotFoundError(ConfigError): 20 | """ Error with no config file found. """ 21 | 22 | 23 | """ State Errors """ 24 | class InvalidStateError(MudPiError): 25 | """ When a problem occurs with impromper state machine states. """ 26 | 27 | 28 | """ Extension Errors """ 29 | class ExtensionNotFound(MudPiError): 30 | """ Error when problem importing extensions """ 31 | def __init__(self, extension): 32 | super().__init__(f"Extension '{extension}' not found.") 33 | self.extension = extension 34 | 35 | class RecursiveDependency(MudPiError): 36 | """ Error when extension references anther extension in dependency loop """ 37 | 38 | def __init__(self, extension, dependency): 39 | super().__init__(f'Recursive dependency loop: {extension} -> {dependency}.') 40 | self.extension = extension 41 | self.dependency = dependency -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a detailed Bug Report to help MudPi improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **List any error messages / strack trace** 14 | ``` 15 | Paste the error message here 16 | ``` 17 | 18 | **To Reproduce** 19 | Steps to reproduce the behavior: 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected behavior** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Debug Logs** 29 | Please run MudPi in debug mode and copy your log output file and error output file. 30 | ``` 31 | Paste output log here 32 | ``` 33 | 34 | ``` 35 | Paste error logs here 36 | ``` 37 | 38 | **Screenshots** 39 | If applicable, add screenshots to help explain your problem. 40 | 41 | **Hardware (please complete the following information):** 42 | - OS: [e.g. Raspberry OS 1.6.1] 43 | - Board [e.g. raspberry pi 4,] 44 | - MudPi Version [e.g. v0.10.0] 45 | - Any important connected components [e.g. Soil Sensor - GPIO 3] 46 | 47 | **Additional context** 48 | Add any other context about the problem here. Talk about how you run MudPi and any more information that would help reproduce your setup for testing. If you are having a hardware related issue be sure to provide all your connected components and connections. 49 | -------------------------------------------------------------------------------- /examples/custom_extension/grow/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom Extension Example 3 | Provide a good description of what 4 | your extension is adding to MudPi 5 | or what it does. 6 | """ 7 | from mudpi.extensions import BaseExtension 8 | 9 | 10 | # Your extension should extend the BaseExtension class 11 | class Extension(BaseExtension): 12 | # The minimum your extension needs is a namespace. This 13 | # should be the same as your folder name and unique for 14 | # all extensions. Interfaces all components use this namespace. 15 | namespace = 'grow' 16 | 17 | # You can also set an update interval at which components 18 | # should be updated to gather new data / state. 19 | update_interval = 1 20 | 21 | def init(self, config): 22 | """ Prepare the extension and all components """ 23 | # This is called on MudPi start and passed config on start. 24 | # Here is where devices should be setup, connections made, 25 | # components created and added etc. 26 | 27 | # Must return True or an error will be assumed disabling the extension 28 | return True 29 | 30 | def validate(self, config): 31 | """ Validate the extension configuration """ 32 | # Here the extension configuration is passed in before the init() method 33 | # is called. The validate method is used to prepare a valid configuration 34 | # for the extension before initialization. This method should return the 35 | # validated config or raise a ConfigError. 36 | 37 | return config 38 | -------------------------------------------------------------------------------- /mudpi/extensions/sensor/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sensors Extension 3 | Sensors are components that gather data and make it 4 | available to MudPi. Sensors support interfaces to 5 | allow additions of new types of devices easily. 6 | """ 7 | from mudpi.extensions import Component, BaseExtension 8 | 9 | 10 | NAMESPACE = 'sensor' 11 | UPDATE_INTERVAL = 30 12 | 13 | class Extension(BaseExtension): 14 | namespace = NAMESPACE 15 | update_interval = UPDATE_INTERVAL 16 | 17 | def init(self, config): 18 | self.config = config[self.namespace] 19 | 20 | self.manager.init(self.config) 21 | 22 | self.manager.register_component_actions('force_update', action='force_update') 23 | return True 24 | 25 | 26 | 27 | class Sensor(Component): 28 | """ Base Sensor 29 | Base Sensor for all sensor interfaces 30 | """ 31 | 32 | """ Properties """ 33 | @property 34 | def id(self): 35 | """ Unique id or key """ 36 | return self.config.get('key').lower() 37 | 38 | @property 39 | def name(self): 40 | """ Friendly name of control """ 41 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 42 | 43 | @property 44 | def state(self): 45 | """ Return state for the sensor """ 46 | return self._state 47 | 48 | 49 | """ Actions """ 50 | def force_update(self, data=None): 51 | """ Force an update of the component. Useful for testing """ 52 | self.update() 53 | self.store_state() 54 | return True -------------------------------------------------------------------------------- /mudpi/extensions/example/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Sensor Interface 3 | Returns a random number between one 4 | and ten with each update. 5 | """ 6 | import random 7 | from mudpi.extensions import BaseInterface 8 | from mudpi.extensions.sensor import Sensor 9 | 10 | 11 | class Interface(BaseInterface): 12 | 13 | def load(self, config): 14 | """ Load example sensor component from configs """ 15 | sensor = ExampleSensor(self.mudpi, config) 16 | self.add_component(sensor) 17 | return True 18 | 19 | 20 | class ExampleSensor(Sensor): 21 | """ Example Sensor 22 | Returns a random number 23 | """ 24 | 25 | """ Properties """ 26 | @property 27 | def id(self): 28 | """ Return a unique id for the component """ 29 | return self.config['key'] 30 | 31 | @property 32 | def name(self): 33 | """ Return the display name of the component """ 34 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 35 | 36 | @property 37 | def state(self): 38 | """ Return the state of the component (from memory, no IO!) """ 39 | return self._state 40 | 41 | @property 42 | def classifier(self): 43 | """ Classification further describing it, effects the data formatting """ 44 | return self.config.get('classifier', "general") 45 | 46 | 47 | """ Methods """ 48 | def update(self): 49 | """ Get Random data """ 50 | self._state = random.randint(1, self.config.get('data', 10)) 51 | return True 52 | 53 | -------------------------------------------------------------------------------- /mudpi/events/adaptors/__init__.py: -------------------------------------------------------------------------------- 1 | class Adaptor: 2 | """ Base adaptor for pubsub event system """ 3 | 4 | # This key should represent key in configs that it will load form 5 | key = None 6 | 7 | adaptors = {} 8 | 9 | def __init_subclass__(cls, **kwargs): 10 | super().__init_subclass__(**kwargs) 11 | cls.adaptors[cls.key] = cls 12 | 13 | def __init__(self, config={}): 14 | self.config = config 15 | 16 | def connect(self): 17 | """ Authenticate to system and cache connections """ 18 | raise NotImplementedError() 19 | 20 | def disconnect(self): 21 | """ Close active connections and cleanup subscribers """ 22 | raise NotImplementedError() 23 | 24 | def subscribe(self, topic, callback): 25 | """ Listen on a topic and pass event data to callback """ 26 | raise NotImplementedError() 27 | 28 | def unsubscribe(self, topic): 29 | """ Stop listening for events on a topic """ 30 | raise NotImplementedError() 31 | 32 | def publish(self, topic, data=None): 33 | """ Publish an event on the topic """ 34 | raise NotImplementedError() 35 | 36 | """ No need to override this unless necessary """ 37 | def subscribe_once(self, topic, callback): 38 | """ Subscribe to topic for only one event """ 39 | def handle_once(data): 40 | """ Wrapper to unsubscribe after event handled """ 41 | self.unsubscribe(topic) 42 | if callable(callback): 43 | # Pass data to real callback 44 | callback(data) 45 | 46 | return self.subscribe(topic, handle_once) 47 | 48 | def get_message(self): 49 | """ Some protocols need to initate a poll for new messages """ 50 | pass 51 | 52 | # Import adaptors 53 | from . import redis, mqtt 54 | 55 | -------------------------------------------------------------------------------- /mudpi/extensions/redis/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Redis Extension 3 | Includes interfaces for redis to 4 | get data and manage events. 5 | """ 6 | import redis 7 | from mudpi.extensions import BaseExtension 8 | 9 | 10 | class Extension(BaseExtension): 11 | namespace = 'redis' 12 | update_interval = 30 13 | 14 | def init(self, config): 15 | """ Prepare the redis connection and components """ 16 | self.connections = {} 17 | self.config = config 18 | 19 | if not isinstance(config, list): 20 | config = [config] 21 | 22 | # Prepare connections to redis 23 | for conf in config: 24 | host = conf.get('host', '127.0.0.1') 25 | port = conf.get('port', 6379) 26 | password = conf.get('password') 27 | if conf['key'] not in self.connections: 28 | self.connections[conf['key']] = redis.Redis(host=host, port=port, password=password) 29 | 30 | return True 31 | 32 | def validate(self, config): 33 | """ Validate the redis connection info """ 34 | config = config[self.namespace] 35 | if not isinstance(config, list): 36 | config = [config] 37 | 38 | for conf in config: 39 | key = conf.get('key') 40 | if key is None: 41 | raise ConfigError('Redis missing a `key` in config for connection') 42 | 43 | host = conf.get('host') 44 | if host is None: 45 | conf['host'] = '127.0.0.1' 46 | 47 | port = conf.get('port') 48 | if port is None: 49 | conf['port'] = 6379 50 | return config -------------------------------------------------------------------------------- /mudpi/sensors/mcp3xxx/soil_sensor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from adafruit_mcp3xxx.analog_in import AnalogIn 4 | 5 | from logger.Logger import Logger, LOG_LEVEL 6 | from sensors.mcp3xxx.sensor import Sensor 7 | 8 | 9 | 10 | # Tested using Sun3Drucker Model SX239 11 | # Wet Water = 287 12 | # Dry Air = 584 13 | AirBounds = 43700 14 | WaterBounds = 13000 15 | intervals = int((AirBounds - WaterBounds) / 3) 16 | 17 | 18 | class SoilSensor(Sensor): 19 | 20 | def __init__(self, pin, mcp, name=None, key=None, redis_conn=None): 21 | super().__init__(pin, name=name, key=key, mcp=mcp, 22 | redis_conn=redis_conn) 23 | return 24 | 25 | def init_sensor(self): 26 | self.topic = AnalogIn(self.mcp, Sensor.PINS[self.pin]) 27 | 28 | def read(self): 29 | resistance = self.read_pin() 30 | moistpercent = ( 31 | (resistance - WaterBounds) / ( 32 | AirBounds - WaterBounds) 33 | ) * 100 34 | if moistpercent > 80: 35 | moisture = 'Very Dry - ' + str(int(moistpercent)) 36 | elif 80 >= moistpercent > 45: 37 | moisture = 'Dry - ' + str(int(moistpercent)) 38 | elif 45 >= moistpercent > 25: 39 | moisture = 'Wet - ' + str(int(moistpercent)) 40 | else: 41 | moisture = 'Very Wet - ' + str(int(moistpercent)) 42 | # print("Resistance: %d" % resistance) 43 | # TODO: Put redis store into sensor worker 44 | self.r.set(self.key, 45 | resistance) 46 | # TODO: CHANGE BACK TO 'moistpercent' (PERSONAL CONFIG) 47 | 48 | Logger.log(LOG_LEVEL["debug"], "moisture: {0}".format(moisture)) 49 | return resistance 50 | -------------------------------------------------------------------------------- /examples/nanpy_node/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Nanpy Example", 4 | "debug": false, 5 | }, 6 | "sensor": [ 7 | { 8 | "name": "Nanpy DHT", 9 | "interface": "nanpy", 10 | "node": "demo_box_1", 11 | "key": "nanpy_dht_d3", 12 | "type": "dht", 13 | "pin": 2 14 | } 15 | ], 16 | "control": [ 17 | { 18 | "name": "Nanpy Button D0", 19 | "interface": "nanpy", 20 | "node": "demo_box_1", 21 | "key": "nanpy_button_d0", 22 | "type": "button", 23 | "pin": 16 24 | }, 25 | { 26 | "name": "Nanpy Switch D1", 27 | "interface": "nanpy", 28 | "node": "demo_box_1", 29 | "key": "nanpy_switch_d1", 30 | "type": "switch", 31 | "pin": 5 32 | }, 33 | { 34 | "name": "Nanpy Potentiometer A0", 35 | "interface": "nanpy", 36 | "node": "demo_box_1", 37 | "key": "nanpy_pot_a0", 38 | "type": "potentiometer", 39 | "pin": 0 40 | } 41 | ], 42 | "toggle": [ 43 | { 44 | "name": "Nanpy LED GPIO 8", 45 | "interface": "nanpy", 46 | "node": "demo_box_1", 47 | "key": "nanpy_toggle_d8", 48 | "max_duration": 10, 49 | "pin": 15 50 | }, 51 | { 52 | "key": "example_toggle_1", 53 | "interface": "example" 54 | } 55 | ], 56 | "nanpy": [ 57 | { 58 | "name": "Demo Box 1", 59 | "key" : "demo_box_1", 60 | "address": "192.168.2.150", 61 | "use_wifi":true, 62 | "controls":[ 63 | { 64 | "pin":3, 65 | "type":"switch", 66 | "name":"Switch 1", 67 | "key": "switch_1", 68 | "topic": "garden/pi/controls/2" 69 | } 70 | ] 71 | } 72 | ]} -------------------------------------------------------------------------------- /examples/timer/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Timer Example", 4 | "debug": true, 5 | "unit_system": "imperial", 6 | "location": { 7 | "latitude": 30, 8 | "longitude": -89 9 | }, 10 | "events": { 11 | "redis": { 12 | "host": "127.0.0.1", 13 | "port": 6379 14 | }, 15 | "mqtt": { 16 | "host": "localhost" 17 | } 18 | } 19 | }, 20 | "trigger": [ 21 | { 22 | "interface": "state", 23 | "source": "example_1", 24 | "key": "trigger_timer_start", 25 | "name": "Start Example Timer", 26 | "frequency": "once", 27 | "actions": [ 28 | ".trigger_timer_1.start" 29 | ], 30 | "thresholds": [ 31 | { 32 | "comparison": "gte", 33 | "value": 5 34 | } 35 | ] 36 | }, 37 | { 38 | "interface": "timer", 39 | "key": "trigger_timer_1", 40 | "name": "Example Trigger Timer", 41 | "frequency": "many", 42 | "duration": 10, 43 | "actions": [ 44 | "sensor.timer.start" 45 | ] 46 | } 47 | ], 48 | "sensor": [ 49 | { 50 | "interface": "example", 51 | "update_interval": 10, 52 | "key": "example_1", 53 | "data": 5 54 | }, 55 | { 56 | "interface": "timer", 57 | "key": "timer_10s", 58 | "duration": 10 59 | }, 60 | { 61 | "interface": "timer", 62 | "key": "countdown_timer_10s", 63 | "duration": 10, 64 | "invert_count": true 65 | } 66 | ] 67 | } -------------------------------------------------------------------------------- /mudpi/extensions/example/char_display.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Character Display 3 | Stores Message in Memory. 4 | """ 5 | 6 | from mudpi.extensions import BaseInterface 7 | from mudpi.logger.Logger import Logger, LOG_LEVEL 8 | from mudpi.extensions.char_display import CharDisplay 9 | 10 | 11 | class Interface(BaseInterface): 12 | 13 | def load(self, config): 14 | """ Load example display component from configs """ 15 | display = ExampleDisplay(self.mudpi, config) 16 | 17 | # Check for test messages to fill the queue with 18 | if config.get('messages'): 19 | _count = 0 20 | while(_count < display.message_limit): 21 | for msg in config['messages']: 22 | display.add_message({'message': msg}) 23 | _count +=1 24 | 25 | if display: 26 | self.add_component(display) 27 | return True 28 | 29 | 30 | def validate(self, config): 31 | """ Validate the display configs """ 32 | if not isinstance(config, list): 33 | config = [config] 34 | 35 | for conf in config: 36 | if not conf.get('key'): 37 | raise ConfigError('Missing `key` in example display config.') 38 | 39 | return config 40 | 41 | 42 | class ExampleDisplay(CharDisplay): 43 | """ Example Character Display 44 | Test display that keeps messages in memory. 45 | """ 46 | 47 | """ Properties """ 48 | @property 49 | def default_duration(self): 50 | """ Default message display duration """ 51 | return int(self.config.get('default_duration', 10)) 52 | 53 | 54 | """ Actions """ 55 | def clear(self, data=None): 56 | """ Clear the display screen """ 57 | self.current_message = '' 58 | 59 | def show(self, data={}): 60 | """ Show a message on the display """ 61 | if not isinstance(data, dict): 62 | data = {'message': data} 63 | 64 | self.current_message = data.get('message', '') 65 | -------------------------------------------------------------------------------- /mudpi/sensors/arduino/soil_sensor.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | import json 4 | import redis 5 | from .sensor import Sensor 6 | from nanpy import (ArduinoApi, SerialManager) 7 | import sys 8 | 9 | 10 | 11 | import constants 12 | 13 | default_connection = SerialManager(device='/dev/ttyUSB0') 14 | # r = redis.Redis(host='127.0.0.1', port=6379) 15 | 16 | # Wet Water = 287 17 | # Dry Air = 584 18 | AirBounds = 590 19 | WaterBounds = 280 20 | intervals = int((AirBounds - WaterBounds) / 3) 21 | 22 | 23 | class SoilSensor(Sensor): 24 | 25 | def __init__(self, pin, name=None, key=None, connection=default_connection, 26 | redis_conn=None): 27 | super().__init__(pin, name=name, key=key, connection=connection, 28 | redis_conn=redis_conn) 29 | return 30 | 31 | def init_sensor(self): 32 | # read data using pin specified pin 33 | self.api.pinMode(self.pin, self.api.INPUT) 34 | 35 | def read(self): 36 | resistance = self.api.analogRead(self.pin) 37 | moistpercent = ((resistance - WaterBounds) / ( 38 | AirBounds - WaterBounds)) * 100 39 | if (moistpercent > 80): 40 | moisture = 'Very Dry - ' + str(int(moistpercent)) 41 | elif (moistpercent <= 80 and moistpercent > 45): 42 | moisture = 'Dry - ' + str(int(moistpercent)) 43 | elif (moistpercent <= 45 and moistpercent > 25): 44 | moisture = 'Wet - ' + str(int(moistpercent)) 45 | else: 46 | moisture = 'Very Wet - ' + str(int(moistpercent)) 47 | # print("Resistance: %d" % resistance) 48 | # TODO: Put redis store into sensor worker 49 | self.r.set(self.key, 50 | resistance) # TODO: CHANGE BACK TO 'moistpercent' (PERSONAL CONFIG) 51 | return resistance 52 | 53 | def read_raw(self): 54 | resistance = self.api.analogRead(self.pin) 55 | # print("Resistance: %d" % resistance) 56 | self.r.set(self.key + '_raw', resistance) 57 | return resistance 58 | -------------------------------------------------------------------------------- /mudpi/extensions/example/toggle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Toggle Interface 3 | Example toggle for testing. State 4 | is stored in memory. 5 | """ 6 | from mudpi.extensions import BaseInterface 7 | from mudpi.extensions.toggle import Toggle 8 | from mudpi.exceptions import MudPiError, ConfigError 9 | 10 | 11 | class Interface(BaseInterface): 12 | 13 | def load(self, config): 14 | """ Load example toggle component from configs """ 15 | toggle = ExampleToggle(self.mudpi, config) 16 | if toggle: 17 | self.add_component(toggle) 18 | return True 19 | 20 | def validate(self, config): 21 | """ Validate the example config """ 22 | if not isinstance(config, list): 23 | config = [config] 24 | 25 | for conf in config: 26 | if conf.get('key') is None: 27 | raise ConfigError('Missing `key` in example toggle config.') 28 | 29 | return config 30 | 31 | 32 | class ExampleToggle(Toggle): 33 | """ Example Toggle 34 | Turns a boolean off and on in memory 35 | """ 36 | 37 | """ Methods """ 38 | def restore_state(self, state): 39 | """ This is called on start to 40 | restore previous state """ 41 | self.active = True if state.state else False 42 | self.reset_duration() 43 | return 44 | 45 | 46 | """ Actions """ 47 | def toggle(self, data={}): 48 | # Toggle the state 49 | if self.mudpi.is_prepared: 50 | self.active = not self.active 51 | self.store_state() 52 | self.fire() 53 | 54 | def turn_on(self, data={}): 55 | # Turn on if its not on 56 | if self.mudpi.is_prepared: 57 | if not self.active: 58 | self.active = True 59 | self.store_state() 60 | self.fire() 61 | 62 | def turn_off(self, data={}): 63 | # Turn off if its not off 64 | if self.mudpi.is_prepared: 65 | if self.active: 66 | self.active = False 67 | self.store_state() 68 | self.fire() -------------------------------------------------------------------------------- /mudpi/events/adaptors/redis.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import json 3 | from . import Adaptor 4 | from mudpi.utils import decode_event_data 5 | 6 | 7 | class RedisAdaptor(Adaptor): 8 | """ Allow MudPi events on Pubsub through Redis """ 9 | key = 'redis' 10 | callbacks = {} 11 | 12 | def connect(self): 13 | """ Make redis connection and setup pubsub """ 14 | host = self.config.get('host', '127.0.0.1') 15 | port = self.config.get('port', 6379) 16 | password = self.config.get('password') 17 | self.connection = redis.Redis(host=host, port=port, password=password) 18 | self.pubsub = self.connection.pubsub() 19 | return True 20 | 21 | def disconnect(self): 22 | """ Close active connections and cleanup subscribers """ 23 | self.pubsub.close() 24 | self.connection.close() 25 | return True 26 | 27 | def subscribe(self, topic, callback): 28 | """ Listen on a topic and pass event data to callback """ 29 | if topic not in self.callbacks: 30 | self.callbacks[topic] = [callback] 31 | else: 32 | if callback not in self.callbacks[topic]: 33 | self.callbacks[topic].append(callback) 34 | def callback_handler(message): 35 | """ callback handler to allow multiple hanlders on one topic """ 36 | try: 37 | _topic = message["channel"].decode('utf-8') 38 | except Exception as error: 39 | _topic = message["channel"] 40 | if _topic in self.callbacks: 41 | for callbk in self.callbacks[_topic]: 42 | callbk(decode_event_data(message["data"])) 43 | 44 | return self.pubsub.subscribe(**{topic: callback_handler}) 45 | 46 | def unsubscribe(self, topic): 47 | """ Stop listening for events on a topic """ 48 | del self.callbacks[topic] 49 | return self.pubsub.unsubscribe(topic) 50 | 51 | def publish(self, topic, data=None): 52 | """ Publish an event on the topic """ 53 | if data: 54 | return self.connection.publish(topic, json.dumps(data)) 55 | 56 | return self.connection.publish(topic) 57 | 58 | def get_message(self): 59 | """ Check for new messages waiting """ 60 | return self.pubsub.get_message() -------------------------------------------------------------------------------- /examples/custom_extension/grow/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cron Trigger Interface 3 | Cron schedule support for triggers 4 | to allow scheduling. 5 | """ 6 | import time 7 | import pycron 8 | from mudpi.exceptions import ConfigError 9 | from mudpi.extensions import BaseInterface 10 | from mudpi.extensions.trigger import Trigger 11 | from mudpi.logger.Logger import Logger, LOG_LEVEL 12 | 13 | 14 | class Interface(BaseInterface): 15 | 16 | # Override the update time 17 | update_interval = 60 18 | 19 | def load(self, config): 20 | """ Load cron trigger component from configs """ 21 | trigger = CronTrigger(self.mudpi, config) 22 | if trigger: 23 | self.add_component(trigger) 24 | return True 25 | 26 | def validate(self, config): 27 | """ Validate the trigger config """ 28 | if not isinstance(config, list): 29 | config = [config] 30 | 31 | for conf in config: 32 | if not conf.get('schedule'): 33 | Logger.log( 34 | LOG_LEVEL["debug"], 35 | 'Trigger: No `schedule`, defaulting to every 5mins' 36 | ) 37 | # raise ConfigError('Missing `schedule` in Trigger config.') 38 | 39 | return config 40 | 41 | 42 | class CronTrigger(Trigger): 43 | """ A trigger that resoponds to time 44 | changes based on cron schedule string 45 | """ 46 | 47 | """ Properties """ 48 | @property 49 | def schedule(self): 50 | """ Cron schedule string to check time against """ 51 | return self.config.get('schedule', '*/5 * * * *') 52 | 53 | 54 | """ Methods """ 55 | def init(self): 56 | """ Pass call to parent """ 57 | super().init() 58 | 59 | def check(self): 60 | """ Check trigger schedule thresholds """ 61 | if self.mudpi.is_running: 62 | try: 63 | if pycron.is_now(self.schedule): 64 | if not self.active: 65 | self.trigger() 66 | self.active = True 67 | else: 68 | self.active = False 69 | except Exception as error: 70 | Logger.log( 71 | LOG_LEVEL["error"], 72 | "Error evaluating time trigger schedule." 73 | ) 74 | return 75 | 76 | -------------------------------------------------------------------------------- /mudpi/extensions/cron/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cron Trigger Interface 3 | Cron schedule support for triggers 4 | to allow scheduling. 5 | """ 6 | import time 7 | import pycron 8 | import datetime 9 | from mudpi.exceptions import ConfigError 10 | from mudpi.extensions import BaseInterface 11 | from mudpi.extensions.trigger import Trigger 12 | from mudpi.logger.Logger import Logger, LOG_LEVEL 13 | 14 | 15 | class Interface(BaseInterface): 16 | 17 | # Override the update time 18 | update_interval = 60 19 | 20 | def load(self, config): 21 | """ Load cron trigger component from configs """ 22 | trigger = CronTrigger(self.mudpi, config) 23 | if trigger: 24 | self.add_component(trigger) 25 | return True 26 | 27 | def validate(self, config): 28 | """ Validate the trigger config """ 29 | if not isinstance(config, list): 30 | config = [config] 31 | 32 | for conf in config: 33 | if not conf.get('schedule'): 34 | Logger.log( 35 | LOG_LEVEL["debug"], 36 | 'Trigger: No `schedule`, defaulting to every 5 mins' 37 | ) 38 | # raise ConfigError('Missing `schedule` in Trigger config.') 39 | 40 | return config 41 | 42 | 43 | class CronTrigger(Trigger): 44 | """ A trigger that resoponds to time 45 | changes based on cron schedule string 46 | """ 47 | 48 | """ Properties """ 49 | @property 50 | def schedule(self): 51 | """ Cron schedule string to check time against """ 52 | return self.config.get('schedule', '*/5 * * * *') 53 | 54 | 55 | """ Methods """ 56 | def init(self): 57 | """ Pass call to parent """ 58 | super().init() 59 | 60 | def check(self): 61 | """ Check trigger schedule thresholds """ 62 | if self.mudpi.is_running: 63 | try: 64 | if pycron.is_now(self.schedule): 65 | self.trigger() 66 | if not self.active: 67 | self.active = True 68 | else: 69 | if self.active: 70 | self.active = False 71 | except Exception as error: 72 | Logger.log( 73 | LOG_LEVEL["error"], 74 | "Error evaluating time trigger schedule." 75 | ) 76 | return -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | scripts/ 4 | tests/ 5 | img/ 6 | build/ 7 | media/ 8 | mudpi.config 9 | *.log 10 | # Python compiled 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | cover/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | .pybuilder/ 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | # For a library or package, you might want to ignore these files since the code is 96 | # intended to run in multiple environments; otherwise, check them in: 97 | # .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # IDE configs 133 | .idea/ 134 | .DS_Store -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | ## Description 4 | 5 | Please include a summary of the change and link any issues fixed. Please also include relevant motivation and context. Provide thorough documentation of all changes. List any new dependencies that are required. 6 | 7 | Fixes # (issue) 8 | 9 | @mentions the author(s) 10 | 11 | ## Type of change 12 | 13 | Please delete options that are not relevant. 14 | 15 | - [ ] Bug fix (non-breaking change which fixes an issue) 16 | - [ ] New feature (non-breaking change which adds functionality) 17 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 18 | - [ ] This change has a documentation update 19 | 20 | ## How Has This Been Tested? 21 | 22 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration including needed components. 23 | 24 | - [ ] Test A 25 | - [ ] Test B 26 | 27 | **Test Configuration**: 28 | * Python version: 29 | * Hardware: 30 | * Toolchain: 31 | * OS: 32 | 33 | ## New Pull Request Checklist: 34 | 35 | - [ ] My code follows the style guidelines of this project 36 | - [ ] I have performed a self-review of my own code 37 | - [ ] I have read the [contributing documentation.](https://mudpi.app/docs/contributing) 38 | - [ ] I have commented my code, particularly in hard-to-understand areas 39 | - [ ] I have provided corresponding changes to the documentation or included new files 40 | - [ ] My documentation follows the format and style of the [example documentation.](https://github.com/mudpi/mudpi-core/tree/master/.github/DOCS) 41 | - [ ] I have tested that my fix is effective or that my feature works 42 | - [ ] This change/feature does not already exist and is not in another pending PR 43 | - [ ] Any dependent changes have been merged and published in downstream modules 44 | - [ ] I have checked my code and corrected any misspellings 45 | 46 | 47 | ## Documentation 48 | Please provide thorough documentation of your changes and features in a markdown format. You can find a [example of the documentation here.](https://github.com/mudpi/mudpi-core/tree/master/.github) If you are adding a new extension you may attach this as a separate file. It is important that you document your features well so that someone with no information can familiarize themselves and even help collaborate! 49 | 50 | ## Screenshots / Media 51 | If relevant, provide screenshots or any media that would help showcase the proposed changes. 52 | -------------------------------------------------------------------------------- /mudpi/extensions/gpio/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | GPIO Sensor Interface 3 | Connects to a linux board GPIO to 4 | take analog or digital readings. 5 | """ 6 | import re 7 | 8 | import board 9 | import digitalio 10 | from mudpi.extensions import BaseInterface 11 | from mudpi.extensions.sensor import Sensor 12 | from mudpi.exceptions import MudPiError, ConfigError 13 | 14 | 15 | class Interface(BaseInterface): 16 | 17 | def load(self, config): 18 | """ Load GPIO sensor component from configs """ 19 | sensor = GPIOSensor(self.mudpi, config) 20 | if sensor: 21 | self.add_component(sensor) 22 | return True 23 | 24 | def validate(self, config): 25 | """ Validate the dht config """ 26 | if not isinstance(config, list): 27 | config = [config] 28 | 29 | for conf in config: 30 | if not conf.get('pin'): 31 | raise ConfigError('Missing `pin` in GPIO config.') 32 | 33 | return config 34 | 35 | 36 | class GPIOSensor(Sensor): 37 | """ GPIO Sensor 38 | Returns a reading from gpio pin 39 | """ 40 | 41 | """ Properties """ 42 | @property 43 | def id(self): 44 | """ Return a unique id for the component """ 45 | return self.config['key'] 46 | 47 | @property 48 | def name(self): 49 | """ Return the display name of the component """ 50 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 51 | 52 | @property 53 | def state(self): 54 | """ Return the state of the component (from memory, no IO!) """ 55 | return self._state 56 | 57 | @property 58 | def classifier(self): 59 | """ Classification further describing it, effects the data formatting """ 60 | return self.config.get('classifier', 'general') 61 | 62 | @property 63 | def pin(self): 64 | """ Return a pin for the component """ 65 | return self.config['pin'] 66 | 67 | 68 | """ Methods """ 69 | def init(self): 70 | """ Connect to the device """ 71 | self.pin_obj = getattr(board, self.config['pin']) 72 | 73 | if re.match(r'D\d+$', self.pin): 74 | self.is_digital = True 75 | elif re.match(r'A\d+$', self.pin): 76 | self.is_digital = False 77 | else: 78 | self.is_digital = True 79 | 80 | self.gpio = digitalio 81 | 82 | return True 83 | 84 | def update(self): 85 | """ Get data from GPIO connection""" 86 | if self.is_digital: 87 | data = self.gpio.DigitalInOut(self.pin_obj).value 88 | else: 89 | data = self.gpio.AnalogIn(self.pin_obj).value 90 | self._state = data 91 | return data 92 | -------------------------------------------------------------------------------- /mudpi/events/adaptors/mqtt.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import random 4 | import paho.mqtt.client as mqtt 5 | 6 | from . import Adaptor 7 | 8 | 9 | class MQTTAdaptor(Adaptor): 10 | """ Provide pubsub events over MQTT """ 11 | key = 'mqtt' 12 | 13 | connected = False 14 | loop_started = False 15 | callbacks = {} 16 | 17 | def connect(self): 18 | """ Make mqtt connection and setup broker """ 19 | 20 | def on_conn(client, userdata, flags, rc): 21 | if rc == 0: 22 | self.connected = True 23 | 24 | host = self.config.get('host', "localhost") 25 | # port = self.config.get('port', 1883) 26 | self.connection = mqtt.Client(f'mudpi-{random.randint(0, 100)}') 27 | self.connection.on_connect = on_conn 28 | username = self.config.get('username') 29 | password = self.config.get('password') 30 | if all([username, password]): 31 | self.connection.username_pw_set(username, password) 32 | self.connection.connect(host) 33 | while not self.connected: 34 | self.get_message() 35 | time.sleep(0.1) 36 | return True 37 | 38 | def disconnect(self): 39 | """ Close active connections and cleanup subscribers """ 40 | self.connection.loop_stop() 41 | self.connection.disconnect() 42 | return True 43 | 44 | def subscribe(self, topic, callback): 45 | """ Listen on a topic and pass event data to callback """ 46 | if topic not in self.callbacks: 47 | self.callbacks[topic] = [callback] 48 | else: 49 | if callback not in self.callbacks[topic]: 50 | self.callbacks[topic].append(callback) 51 | 52 | def callback_handler(client, userdata, message): 53 | # log = f"{message.payload.decode()} {message.topic}" 54 | if message.topic in self.callbacks: 55 | for callbk in self.callbacks[message.topic]: 56 | callbk(message.payload) 57 | 58 | self.connection.on_message = callback_handler 59 | return self.connection.subscribe(topic) 60 | 61 | def unsubscribe(self, topic): 62 | """ Stop listening for events on a topic """ 63 | del self.callbacks[topic] 64 | return self.connection.unsubscribe(topic) 65 | 66 | def publish(self, topic, data=None): 67 | """ Publish an event on the topic """ 68 | if data: 69 | return self.connection.publish(topic, json.dumps(data)) 70 | 71 | return self.connection.publish(topic) 72 | 73 | def get_message(self): 74 | """ Check for new messages waiting """ 75 | if not self.loop_started: 76 | self.connection.loop_start() 77 | self.loop_started = True -------------------------------------------------------------------------------- /mudpi/extensions/group/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Group Trigger Interface 3 | Allows triggers to be grouped 4 | together for complex conditions. 5 | """ 6 | from mudpi.exceptions import ConfigError 7 | from mudpi.extensions import BaseInterface 8 | from mudpi.extensions.trigger import Trigger 9 | from mudpi.logger.Logger import Logger, LOG_LEVEL 10 | 11 | 12 | class Interface(BaseInterface): 13 | 14 | def load(self, config): 15 | """ Load group trigger component from configs """ 16 | trigger = GroupTrigger(self.mudpi, config) 17 | if trigger: 18 | self.add_component(trigger) 19 | return True 20 | 21 | def validate(self, config): 22 | """ Validate the trigger config """ 23 | if not isinstance(config, list): 24 | config = [config] 25 | 26 | for conf in config: 27 | if not conf.get('triggers'): 28 | raise ConfigError('Missing `triggers` keys in Trigger Group') 29 | 30 | return config 31 | 32 | 33 | class GroupTrigger(Trigger): 34 | """ A Group to allow complex combintations 35 | between multiple trigger types. 36 | """ 37 | 38 | """ Properties """ 39 | @property 40 | def triggers(self): 41 | """ Keys of triggers to group """ 42 | return self.config.get('triggers', []) 43 | 44 | @property 45 | def trigger_states(self): 46 | """ Keys of triggers to group """ 47 | return [trigger.active for trigger in self._triggers] 48 | 49 | 50 | """ Methods """ 51 | def init(self): 52 | """ Load in the triggers for the group """ 53 | # List of triggers to monitor 54 | self._triggers = [] 55 | 56 | # Doesnt call super().init() because that is for non-groups 57 | self.cache = self.mudpi.cache.get('trigger', {}) 58 | self.cache.setdefault('groups', {})[self.id] = self 59 | 60 | for _trigger in self.triggers: 61 | _trig = self.cache.get('triggers', {}).get(_trigger) 62 | if _trig: 63 | self.add_trigger(_trig) 64 | return True 65 | 66 | def add_trigger(self, trigger): 67 | """ Add a trigger to monitor """ 68 | self._triggers.append(trigger) 69 | 70 | def check(self): 71 | """ Check if trigger should fire """ 72 | if all(self.trigger_states): 73 | self.active = True 74 | if self._previous_state != self.active: 75 | # Trigger is reset, Fire 76 | self.trigger() 77 | else: 78 | # Trigger not reset check if its multi fire 79 | if self.frequency == 'many': 80 | self.trigger() 81 | else: 82 | self.active = False 83 | self._previous_state = self.active 84 | -------------------------------------------------------------------------------- /mudpi/extensions/control/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Controls Extension 3 | Controls are components like buttons, switches, 4 | potentiometers, etc. They are utilized to get 5 | user input into the system. 6 | """ 7 | import datetime 8 | from mudpi.extensions import Component, BaseExtension 9 | 10 | 11 | NAMESPACE = 'control' 12 | 13 | class Extension(BaseExtension): 14 | namespace = NAMESPACE 15 | update_interval = 0.5 16 | 17 | def init(self, config): 18 | self.config = config[self.namespace] 19 | 20 | self.manager.init(self.config) 21 | return True 22 | 23 | 24 | class Control(Component): 25 | """ Base Control 26 | Base class for all controls. 27 | """ 28 | 29 | @property 30 | def id(self): 31 | """ Unique id or key """ 32 | return self.config.get('key').lower() 33 | 34 | @property 35 | def name(self): 36 | """ Friendly name of control """ 37 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 38 | 39 | @property 40 | def pin(self): 41 | """ The GPIO pin """ 42 | return self.config.get('pin') 43 | 44 | @property 45 | def resistor(self): 46 | """ Set internal resistor to pull UP or DOWN """ 47 | return self.config.get('resistor') 48 | 49 | @property 50 | def debounce(self): 51 | """ Used to smooth out ripples and false fires """ 52 | return self.config.get('debounce') 53 | 54 | @property 55 | def type(self): 56 | """ Button, Switch, Potentiometer """ 57 | return self.config.get('type', 'button').lower() 58 | 59 | @property 60 | def edge_detection(self): 61 | """ Return if edge detection is used """ 62 | _edge_detection = self.config.get('edge_detection').lower() 63 | if _edge_detection is not None: 64 | if _edge_detection == "falling" or _edge_detection == "fell": 65 | _edge_detection = "fell" 66 | elif _edge_detection == "rising" or _edge_detection == "rose": 67 | _edge_detection = "rose" 68 | elif _edge_detection == "both": 69 | _edge_detection = "both" 70 | return _edge_detection 71 | 72 | @property 73 | def invert_state(self): 74 | """ Set to True to make OFF state fire events instead of ON state """ 75 | return self.config.get('invert_state', False) 76 | 77 | 78 | """ Methods """ 79 | def fire(self, data={}): 80 | """ Fire a control event """ 81 | event_data = { 82 | 'event': 'ControlUpdated', 83 | 'component_id': self.id, 84 | 'type': self.type, 85 | 'name': self.name, 86 | 'updated_at': str(datetime.datetime.now().replace(microsecond=0)), 87 | 'state': self.state, 88 | 'invert_state': self.invert_state 89 | } 90 | event_data.update(data) 91 | self.mudpi.events.publish(NAMESPACE, event_data) 92 | self._fired = True -------------------------------------------------------------------------------- /mudpi/tools/event_send_tool.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import threading 3 | import json 4 | import time 5 | 6 | 7 | def timed_message(message, delay=3): 8 | for s in range(1, delay): 9 | remainingTime = delay - s 10 | print(message + '...{0}s \r'.format(remainingTime), end="", flush=True) 11 | time.sleep(s) 12 | 13 | 14 | if __name__ == "__main__": 15 | try: 16 | option = True 17 | message = {} 18 | r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True) 19 | publisher = r 20 | topic = None 21 | while option != 0: 22 | # Clear the screen command 23 | print(chr(27) + "[2J") 24 | print('--------- Redis MudPi ---------') 25 | print('|3. Test Event |') 26 | print('|2. Toggle |') 27 | print('|1. Switch |') 28 | print('|0. Shutdown |') 29 | print('-------------------------------') 30 | 31 | try: 32 | option = int(input('Enter Option: ')) 33 | except: 34 | # Catch string input error 35 | option = 9 36 | 37 | if option != 0: 38 | if option == 1: 39 | try: 40 | new_state = int( 41 | input('Enter State to switch to (0 or 1): ')) 42 | if new_state != 0 and new_state != 1: 43 | new_state = 0 44 | except: 45 | new_state = 0 46 | message = { 47 | 'event': 'Switch', 48 | 'data': new_state 49 | } 50 | 51 | elif option == 2: 52 | message = { 53 | 'event': 'Toggle', 54 | 'data': None 55 | } 56 | 57 | elif option == 3: 58 | message = { 59 | 'event': "StateChanged", 60 | 'data': "/home/pi/Desktop/mudpi/img/mudpi-0039-2019-04-14-02-21.jpg", 61 | 'source': "camera_1" 62 | } 63 | topic = 'garden/pi/camera' 64 | 65 | else: 66 | timed_message('Option not recognized') 67 | print(chr(27) + "[2J") 68 | continue 69 | 70 | if topic is None: 71 | topic = str(input('Enter Topic to Broadcast: ')) 72 | 73 | if topic is not None and topic != '': 74 | # Publish the message 75 | publisher.publish(topic, json.dumps(message)) 76 | print(message) 77 | timed_message('Message Successfully Published!') 78 | else: 79 | timed_message('Topic Input Invalid') 80 | time.sleep(2) 81 | 82 | print('Exit') 83 | 84 | except KeyboardInterrupt: 85 | # Kill The Server 86 | # r.publish('test', json.dumps({'EXIT':True})) 87 | print('Publish Program Terminated...') 88 | 89 | finally: 90 | pass 91 | -------------------------------------------------------------------------------- /mudpi/extensions/example/control.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Control Interface 3 | Example control that randomly fires 4 | events for demonstration. 5 | """ 6 | import random 7 | from mudpi.extensions import BaseInterface 8 | from mudpi.extensions.control import Control 9 | from mudpi.exceptions import MudPiError, ConfigError 10 | 11 | 12 | class Interface(BaseInterface): 13 | 14 | # Examples don't need to be ultra fast 15 | update_interval = 3 16 | 17 | def load(self, config): 18 | """ Load example control component from configs """ 19 | control = ExampleControl(self.mudpi, config) 20 | if control: 21 | self.add_component(control) 22 | return True 23 | 24 | def validate(self, config): 25 | """ Validate the control config """ 26 | if not isinstance(config, list): 27 | config = [config] 28 | 29 | for conf in config: 30 | if conf.get('key') is None: 31 | raise ConfigError('Missing `key` in example control.') 32 | 33 | return config 34 | 35 | 36 | 37 | class ExampleControl(Control): 38 | """ Example Control 39 | Randomly fires active for demonstration of controls 40 | """ 41 | 42 | 43 | """ Properties """ 44 | @property 45 | def state(self): 46 | """ Return the state of the component (from memory, no IO!) """ 47 | return self._state 48 | 49 | @property 50 | def update_chance(self): 51 | """ Return the chance the trigger will fire (1-100) 52 | Default: 25% """ 53 | _chance = self.config.get('update_chance', 25) 54 | if _chance > 100 or _chance < 1: 55 | _chance = 25 56 | return _chance 57 | 58 | @property 59 | def state_changed(self): 60 | """ Return if the state changed from previous state""" 61 | return self.previous_state != self._state 62 | 63 | 64 | """ Methods """ 65 | def init(self): 66 | """ Initalize the control """ 67 | 68 | # Default inital state 69 | self._state = False 70 | 71 | # One time firing 72 | self._fired = False 73 | 74 | # Used to track changes 75 | self.previous_state = self._state 76 | 77 | def update(self): 78 | """ Check if control should flip state randomly """ 79 | _state = self._state 80 | if random.randint(1, 100) <= self.update_chance: 81 | _state = not _state 82 | self.previous_state = self._state 83 | self._state = _state 84 | self.handle_state() 85 | 86 | def handle_state(self): 87 | """ Control logic depending on type of control """ 88 | if self.state_changed: 89 | self.fire() 90 | else: 91 | self._fired = False 92 | 93 | # if self.type == 'button': 94 | # if self._state: 95 | # if not self.invert_state: 96 | # self.fire() 97 | # else: 98 | # if self.invert_state: 99 | # self.fire() 100 | # elif self.type == 'switch': 101 | # # Switches use debounce ensuring we only fire once 102 | # if self._state and not self._fired: 103 | # # State changed since we are using edge detect 104 | # self.fire() 105 | # self._fired = True 106 | # else: 107 | # self._fired = False 108 | 109 | -------------------------------------------------------------------------------- /mudpi/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import socket 4 | import inspect 5 | import subprocess 6 | from mudpi.extensions import Component, BaseExtension, BaseInterface 7 | 8 | def get_ip(): 9 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 10 | try: 11 | # doesn't even have to be reachable 12 | s.connect(('10.255.255.255', 1)) 13 | IP = s.getsockname()[0] 14 | except Exception: 15 | IP = '127.0.0.1' 16 | finally: 17 | s.close() 18 | return IP 19 | 20 | 21 | def get_module_classes(module_name): 22 | """ Get all the classes from a module """ 23 | clsmembers = inspect.getmembers(sys.modules[module_name], inspect.isclass) 24 | return clsmembers 25 | 26 | 27 | def decode_event_data(message): 28 | if isinstance(message, dict): 29 | # print('Dict Found') 30 | return message 31 | elif isinstance(message.decode('utf-8'), str): 32 | try: 33 | temp = json.loads(message.decode('utf-8')) 34 | # print('Json Found') 35 | return temp 36 | except Exception as error: 37 | # print('Json Error. Str Found') 38 | return message.decode('utf-8') #{'event': 'Unknown', 'data': message} 39 | else: 40 | # print('Failed to detect type') 41 | return {'event': 'Unknown', 'data': message} 42 | 43 | 44 | def install_package(package, upgrade=False, target=None): 45 | """ 46 | Install a PyPi package with pip in the background. 47 | Returns boolean. 48 | """ 49 | pip_args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] 50 | if upgrade: 51 | pip_args.append('--upgrade') 52 | if target: 53 | pip_args += ['--target', os.path.abspath(target)] 54 | try: 55 | return 0 == subprocess.call(pip_args) 56 | except subprocess.SubprocessError: 57 | return False 58 | 59 | 60 | def is_package_installed(package): 61 | """ Check if a package is already installed """ 62 | reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) 63 | if '==' not in package: 64 | installed_packages = [r.decode().split('==')[0].lower() for r in reqs.split()] 65 | else: 66 | installed_packages = [r.decode().lower() for r in reqs.split()] 67 | 68 | return package in installed_packages 69 | 70 | 71 | def is_extension(cls): 72 | """ Check if a class is a MudPi Extension. 73 | Accepts class or instance of class 74 | """ 75 | if not inspect.isclass(cls): 76 | if hasattr(cls, '__class__'): 77 | cls = cls.__class__ 78 | else: 79 | return False 80 | return issubclass(cls, BaseExtension) 81 | 82 | 83 | def is_interface(cls): 84 | """ Check if a class is a MudPi Extension. 85 | Accepts class or instance of class 86 | """ 87 | if not inspect.isclass(cls): 88 | if hasattr(cls, '__class__'): 89 | cls = cls.__class__ 90 | else: 91 | return False 92 | return issubclass(cls, BaseInterface) 93 | 94 | 95 | def is_component(cls): 96 | """ Check if a class is a MudPi component. 97 | Accepts class or instance of class 98 | """ 99 | if not inspect.isclass(cls): 100 | if hasattr(cls, '__class__'): 101 | cls = cls.__class__ 102 | else: 103 | return False 104 | return issubclass(cls, Component) -------------------------------------------------------------------------------- /examples/nfc/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Testbench", 4 | "debug": true, 5 | "unit_system": "imperial", 6 | "location": { 7 | "latitude": 42.526, 8 | "longitude": -89.043 9 | }, 10 | "events": { 11 | "redis": { 12 | "host": "127.0.0.1", 13 | "port": 6379 14 | }, 15 | "mqtt": { 16 | "host": "localhost", 17 | "username": "admin", 18 | "password": "admin" 19 | } 20 | } 21 | }, 22 | "sensor": [ 23 | { 24 | "key": "nfc_tag_1", 25 | "interface": "nfc", 26 | "name": "MudPi NFC Tag 1", 27 | "tag_uid": "676d6956-98a1-4d77-904c-a1b2f12f9a38", 28 | "tag_id": "0471c0012e3403", 29 | "reader_id": "nfc_usb_reader", 30 | "security": 0 31 | } 32 | ], 33 | "nfc": { 34 | "key": "nfc_usb_reader", 35 | "model": "ACR122U", 36 | "beep_enabled": true, 37 | "tracking": true, 38 | "persist_records": true, 39 | "save_tags": true, 40 | "writing": true, 41 | "address": "usb:072f:2200", 42 | "store_logs": true, 43 | "log_length": 10, 44 | "default_records": [ 45 | { 46 | "type": "text", 47 | "data": "MudPi Secret Card", 48 | "position": 0 49 | }, 50 | { 51 | "type": "uri", 52 | "data": "https://mudpi.app/docs/extension-nfc", 53 | "position": 1 54 | } 55 | ], 56 | "tags": { 57 | "0471c0012e3403": { 58 | "tag_id": "0471c0012e3403", 59 | "capacity": 492, 60 | "used_capacity": 215, 61 | "writeable": true, 62 | "readable": true, 63 | "count": 0, 64 | "tag_uid": "676d6956-98a1-4d77-904c-a1b2f12f9a38", 65 | "ndef": [ 66 | "MudPi Secret Access Card", 67 | "https://mudpi.app/docs/extension-nfc", 68 | "Secret Data Card Unlocked ", 69 | "Agent: Drake G", 70 | "last_scan:2021-04-15 09:25:20" 71 | ], 72 | "default_records": [ 73 | { 74 | "type": "text", 75 | "data": "MudPi Secret Access Card", 76 | "position": 0 77 | }, 78 | { 79 | "type": "uri", 80 | "data": "https://mudpi.app/docs/extension-nfc", 81 | "position": 1 82 | } 83 | ] 84 | }, 85 | "0471aa01fd3403": { 86 | "tag_id": "0471aa01fd3403", 87 | "capacity": 492, 88 | "used_capacity": 154, 89 | "writeable": true, 90 | "readable": true, 91 | "tag_uid": "ed870f4d-1939-4810-9059-46a10e7269cf", 92 | "count": 0, 93 | "ndef": [ 94 | "MudPi Secret Card", 95 | "https://mudpi.app/docs/extension-nfc", 96 | "last_scan:2021-04-15 01:32:42" 97 | ] 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /mudpi/extensions/picamera/camera.py: -------------------------------------------------------------------------------- 1 | """ 2 | Picamera Interface 3 | Connects to a raspberry pi camera 4 | through the picamera library. 5 | """ 6 | from picamera import PiCamera 7 | from mudpi.exceptions import ConfigError 8 | from mudpi.extensions import BaseInterface 9 | from mudpi.extensions.camera import Camera 10 | from mudpi.logger.Logger import Logger, LOG_LEVEL 11 | 12 | 13 | class Interface(BaseInterface): 14 | 15 | def load(self, config): 16 | """ Load pi camera component from configs """ 17 | camera = RaspberryPiCamera(self.mudpi, config) 18 | if camera: 19 | self.add_component(camera) 20 | return True 21 | 22 | def validate(self, config): 23 | """ Validate the camera config """ 24 | if not isinstance(config, list): 25 | config = [config] 26 | 27 | for conf in config: 28 | if not conf.get('path'): 29 | raise ConfigError('Camera needs a `path` to save files to.') 30 | 31 | return config 32 | 33 | 34 | class RaspberryPiCamera(Camera): 35 | """ Base Camera 36 | Base Camera for all camera interfaces 37 | """ 38 | 39 | """ Properties """ 40 | @property 41 | def record_video(self): 42 | """ Set to True to record video instead of photos """ 43 | return self.config.get('record_video', False) 44 | 45 | 46 | """ Methods """ 47 | def init(self): 48 | """ Prepare the Picamera """ 49 | self.camera = PiCamera( 50 | resolution=(self.width, self.height)) 51 | # Below we calibrate the camera for consistent imaging 52 | self.camera.framerate = self.framerate 53 | # Wait for the automatic gain control to settle 54 | time.sleep(2) 55 | # Now fix the values 56 | self.camera.shutter_speed = self.camera.exposure_speed 57 | self.camera.exposure_mode = 'off' 58 | g = self.camera.awb_gains 59 | self.camera.awb_mode = 'off' 60 | self.camera.awb_gains = g 61 | 62 | def update(self): 63 | """ Main update loop to check when to capture images """ 64 | if self.mudpi.is_prepared: 65 | if self.duration > self.delay.total_seconds(): 66 | if self.record_video: 67 | self.capture_recording(duration=self.record_duration) 68 | else: 69 | self.capture_image() 70 | self.reset_duration() 71 | 72 | 73 | """ Actions """ 74 | def capture_image(self, data={}): 75 | """ Capture a single image from the camera 76 | it should use the file name and increment 77 | counter for sequenetial images """ 78 | if self.camera: 79 | image_name = f'{os.path.join(self.path, self.filename)}.jpg' 80 | self.camera.capture(image_name) 81 | self.last_image = os.path.abspath(image_name) 82 | self.increment_count() 83 | self.fire({'event': 'ImageCaptured', 'image': image_name}) 84 | 85 | def capture_recording(self, data={}): 86 | """ Record a video from the camera """ 87 | _duration = data.get('duration', 5) 88 | if self.camera: 89 | _file_name = f'{os.path.join(self.path, self.filename)}.h264' 90 | self.camera.start_recording(_file_name) 91 | self.camera.wait_recording(_duration) 92 | self.camera.stop_recording() 93 | self.last_image = os.path.abspath(_file_name) 94 | self.increment_count() 95 | self.fire({'event': 'RecordingCaptured', 'file': _file_name}) 96 | -------------------------------------------------------------------------------- /mudpi/extensions/toggle/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Toggle Trigger Interface 3 | Monitors control state changes and 4 | checks new state against any 5 | thresholds if provided. 6 | """ 7 | from mudpi.utils import decode_event_data 8 | from mudpi.exceptions import ConfigError 9 | from mudpi.extensions import BaseInterface 10 | from mudpi.extensions.trigger import Trigger 11 | from mudpi.logger.Logger import Logger, LOG_LEVEL 12 | 13 | 14 | class Interface(BaseInterface): 15 | 16 | def load(self, config): 17 | """ Load toggle Trigger component from configs """ 18 | trigger = ToggleTrigger(self.mudpi, config) 19 | if trigger: 20 | self.add_component(trigger) 21 | return True 22 | 23 | def validate(self, config): 24 | """ Validate the trigger config """ 25 | if not isinstance(config, list): 26 | config = [config] 27 | 28 | for conf in config: 29 | if not conf.get('source'): 30 | raise ConfigError('Missing `source` key in Toggle Trigger config.') 31 | 32 | return config 33 | 34 | 35 | class ToggleTrigger(Trigger): 36 | """ A trigger that listens to states 37 | and checks for new state that 38 | matches any thresholds. 39 | """ 40 | 41 | # Used for onetime subscribe 42 | _listening = False 43 | 44 | 45 | """ Methods """ 46 | def init(self): 47 | """ Listen to the state for changes """ 48 | super().init() 49 | if self.mudpi.is_prepared: 50 | if not self._listening: 51 | # TODO: Eventually get a handler returned to unsub just this listener 52 | self.mudpi.events.subscribe('toggle', self.handle_event) 53 | self._listening = True 54 | return True 55 | 56 | def handle_event(self, event): 57 | """ Handle the event data from the event system """ 58 | _event_data = decode_event_data(event) 59 | 60 | if _event_data == self._last_event: 61 | # Event already handled 62 | return 63 | 64 | self._last_event = _event_data 65 | if _event_data.get('event'): 66 | try: 67 | if _event_data['event'] == 'ToggleUpdated': 68 | if _event_data['component_id'] == self.source: 69 | sensor_value = self._parse_data(_event_data["state"]) 70 | if self.evaluate_thresholds(sensor_value): 71 | self.active = True 72 | if self._previous_state != self.active: 73 | # Trigger is reset, Fire 74 | self.trigger(_event_data) 75 | else: 76 | # Trigger not reset check if its multi fire 77 | if self.frequency == 'many': 78 | self.trigger(_event_data) 79 | else: 80 | self.active = False 81 | except Exception as error: 82 | Logger.log(LOG_LEVEL["error"], 83 | f'Error evaluating thresholds for trigger {self.id}') 84 | Logger.log(LOG_LEVEL["debug"], error) 85 | self._previous_state = self.active 86 | 87 | def unload(self): 88 | # Unsubscribe once bus supports single handler unsubscribes 89 | return 90 | 91 | def _parse_data(self, data): 92 | """ Get nested data if set otherwise return the data """ 93 | return data if self.nested_source is None else data.get(self.nested_source, None) 94 | 95 | -------------------------------------------------------------------------------- /mudpi/tools/lcd_message_tool.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import threading 3 | import json 4 | import time 5 | 6 | def timed_message(message, delay=3): 7 | for s in range(1,delay): 8 | remainingTime = delay - s 9 | print(message + '...{0}s \r'.format(remainingTime), end="", flush=True) 10 | time.sleep(s) 11 | 12 | if __name__ == "__main__": 13 | try: 14 | option = True 15 | message = {} 16 | r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True) 17 | publisher = r 18 | topic = None 19 | while option != 0: 20 | #Clear the screen command 21 | print(chr(27) + "[2J") 22 | print('--------- LCD MudPi ---------') 23 | print('|4. Clear Message Queue |') 24 | print('|3. Clear Display |') 25 | print('|2. Test Message |') 26 | print('|1. Add Message |') 27 | print('|0. Shutdown |') 28 | print('-------------------------------') 29 | try: 30 | option = int(input('Enter Option: ')) 31 | except: 32 | #Catch string input error 33 | option = 9 34 | if option != 0: 35 | if option == 1: 36 | try: 37 | msg = { 38 | "message":"", 39 | "duration":10 40 | } 41 | msg["message"] = str(input('Enter Message to Display: ')) 42 | msg["duration"] = int(input('Enter Duration to Display (seconds): ')) 43 | 44 | except: 45 | msg = { 46 | "message":"Error Test", 47 | "duration":10 48 | } 49 | message = { 50 | 'event': 'Message', 51 | 'data': msg 52 | } 53 | elif option == 2: 54 | msg = { 55 | "message":"Test Message\nMudPi Test", 56 | "duration":15 57 | } 58 | message = { 59 | 'event': 'Message', 60 | 'data': msg 61 | } 62 | elif option == 3: 63 | message = { 64 | 'event': 'Clear', 65 | 'data': 1 66 | } 67 | elif option == 4: 68 | message = { 69 | 'event': 'ClearQueue', 70 | 'data': 1 71 | } 72 | else: 73 | timed_message('Option not recognized') 74 | print(chr(27) + "[2J") 75 | continue 76 | 77 | if topic is None: 78 | topic = str(input('Enter the LCD Topic to Broadcast: ')) 79 | 80 | if topic is not None and topic != '': 81 | #Publish the message 82 | publisher.publish(topic, json.dumps(message)) 83 | print(message) 84 | timed_message('Message Successfully Queued!') 85 | else: 86 | timed_message('Topic Input Invalid') 87 | time.sleep(2) 88 | 89 | print('Exit') 90 | except KeyboardInterrupt: 91 | #Kill The Server 92 | #r.publish('test', json.dumps({'EXIT':True})) 93 | print('LCD Message Program Terminated...') 94 | finally: 95 | pass 96 | 97 | -------------------------------------------------------------------------------- /mudpi/extensions/bme280/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | BME280 Sensor Interface 3 | Connects to a BME280 device to get 4 | environment and climate readings. 5 | """ 6 | 7 | import board 8 | import adafruit_bme280 9 | 10 | from busio import I2C 11 | from mudpi.extensions import BaseInterface 12 | from mudpi.extensions.sensor import Sensor 13 | from mudpi.exceptions import MudPiError, ConfigError 14 | 15 | 16 | class Interface(BaseInterface): 17 | 18 | def load(self, config): 19 | """ Load BME280 sensor component from configs """ 20 | sensor = BME280Sensor(self.mudpi, config) 21 | if sensor: 22 | self.add_component(sensor) 23 | return True 24 | 25 | def validate(self, config): 26 | """ Validate the bme280 config """ 27 | if not isinstance(config, list): 28 | config = [config] 29 | 30 | for conf in config: 31 | if not conf.get('key'): 32 | raise ConfigError('Missing `key` in i2c display config.') 33 | 34 | if not conf.get('address'): 35 | # raise ConfigError('Missing `address` in BME280 config.') 36 | conf['address'] = 0x77 37 | else: 38 | addr = conf['address'] 39 | 40 | # Convert hex string/int to actual hex 41 | if isinstance(addr, str): 42 | addr = int(addr, 16) 43 | 44 | conf['address'] = addr 45 | 46 | return config 47 | 48 | 49 | class BME280Sensor(Sensor): 50 | """ BME280 Sensor 51 | Gets readings for pressure, humidity, 52 | temperature and altitude. 53 | """ 54 | 55 | """ Properties """ 56 | @property 57 | def id(self): 58 | """ Return a unique id for the component """ 59 | return self.config['key'] 60 | 61 | @property 62 | def name(self): 63 | """ Return the display name of the component """ 64 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 65 | 66 | @property 67 | def state(self): 68 | """ Return the state of the component (from memory, no IO!) """ 69 | return self._state 70 | 71 | @property 72 | def classifier(self): 73 | """ Classification further describing it, effects the data formatting """ 74 | return 'climate' 75 | 76 | 77 | """ Methods """ 78 | def init(self): 79 | self.i2c = I2C(board.SCL, board.SDA) 80 | self._sensor = adafruit_bme280.Adafruit_BME280_I2C( 81 | self.i2c, address=self.config['address'] 82 | ) 83 | # Change this to match the location's pressure (hPa) at sea level 84 | self._sensor.sea_level_pressure = self.config.get('calibration_pressure', 1013.25) 85 | 86 | return True 87 | 88 | def update(self): 89 | """ Get data from BME280 device""" 90 | temperature = round(self._sensor.temperature * 1.8 + 32, 2) 91 | humidity = round(self._sensor.relative_humidity, 1) 92 | pressure = round(self._sensor.pressure, 2) 93 | altitude = round(self._sensor.altitude, 3) 94 | 95 | if humidity is not None and temperature is not None: 96 | readings = { 97 | 'temperature': temperature, 98 | 'humidity': humidity, 99 | 'pressure': pressure, 100 | 'altitude': altitude 101 | } 102 | self._state = readings 103 | return readings 104 | else: 105 | Logger.log( 106 | LOG_LEVEL["error"], 107 | 'Failed to get reading [BME280]. Try again!' 108 | ) 109 | 110 | return None 111 | -------------------------------------------------------------------------------- /mudpi/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | """ Constants used by MudPi """ 4 | MAJOR_VERSION = 0 5 | MINOR_VERSION = 10 6 | PATCH_VERSION = "0" 7 | __version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}' 8 | 9 | """ PATHS """ 10 | DEFAULT_CONFIG_FILE = "mudpi.config" 11 | PATH_MUDPI = os.getcwd() # /etc/mudpi 12 | PATH_CORE = f"{PATH_MUDPI}/core/mudpi" 13 | PATH_LOGS = f"{PATH_MUDPI}/logs" 14 | PATH_CONFIG = f"{PATH_CORE}" 15 | 16 | """ DEFAULTS """ 17 | DEFAULT_UPDATE_INTERVAL = 30 18 | 19 | """ DATES / TIMES """ 20 | MONTHS = { 21 | 'jan': 'January', 22 | 'feb': 'February', 23 | 'mar': 'March', 24 | 'apr': 'April', 25 | 'may': 'May', 26 | 'jun': 'June', 27 | 'jul': 'July', 28 | 'aug': 'August', 29 | 'sep': 'September', 30 | 'oct': 'October', 31 | 'nov': 'November', 32 | 'dec': 'December' } 33 | WEEKDAYS = { 34 | "mon": 'Monday', 35 | "tue": 'Tuesday', 36 | "wed": 'Wednesday', 37 | "thu": 'Thursday', 38 | "fri": 'Friday', 39 | "sat": 'Saturday', 40 | "sun": 'Sunday' } 41 | 42 | 43 | #### Display Characters ##### 44 | FONT_RESET_CURSOR = "\x1b[1F" 45 | FONT_RED = "\033[1;31m" 46 | FONT_GREEN = '\033[1;32m' 47 | FONT_YELLOW = "\033[1;33m" 48 | FONT_PURPLE = "\033[1;34m" 49 | FONT_MAGENTA = "\033[1;35m" 50 | FONT_CYAN = "\033[1;36m" 51 | FONT_RESET = "\x1b[0m" 52 | RED_BACK = "\x1b[41;37m" 53 | GREEN_BACK = "\x1b[42;30m" 54 | YELLOW_BACK = "\x1b[43;30m" 55 | FONT_PADDING = 52 56 | 57 | 58 | """ COMPONENT CLASSIFIERS """ 59 | CLASSIFIER_BATTERY = "battery" 60 | CLASSIFIER_CURRENT = "current" 61 | CLASSIFIER_EC = "electrical_conductivity" 62 | CLASSIFIER_ENERGY = "energy" 63 | CLASSIFIER_FLOWMETER = "flowmeter" 64 | CLASSIFIER_HUMIDITY = "humidity" 65 | CLASSIFIER_ILLUMINANCE = "illuminance" 66 | CLASSIFIER_LIQUID_LEVEL = "liquid_level" 67 | CLASSIFIER_SIGNAL_STRENGTH = "signal_strength" 68 | CLASSIFIER_TEMPERATURE = "temperature" 69 | CLASSIFIER_TIMESTAMP = "timestamp" 70 | CLASSIFIER_MOISTURE = "moisture" 71 | CLASSIFIER_PH = "ph" 72 | CLASSIFIER_PRESSURE = "pressure" 73 | CLASSIFIER_POWER = "power" 74 | CLASSIFIER_POWER_FACTOR = "power_factor" 75 | CLASSIFIER_VOLTAGE = "voltage" 76 | CLASSIFIERS = [ 77 | CLASSIFIER_BATTERY, 78 | CLASSIFIER_CURRENT, 79 | CLASSIFIER_EC, 80 | CLASSIFIER_ENERGY, 81 | CLASSIFIER_FLOWMETER, 82 | CLASSIFIER_HUMIDITY, 83 | CLASSIFIER_ILLUMINANCE, 84 | CLASSIFIER_SIGNAL_STRENGTH, 85 | CLASSIFIER_TEMPERATURE, 86 | CLASSIFIER_TIMESTAMP, 87 | CLASSIFIER_MOISTURE, 88 | CLASSIFIER_PH, 89 | CLASSIFIER_PRESSURE, 90 | CLASSIFIER_POWER, 91 | CLASSIFIER_POWER_FACTOR, 92 | CLASSIFIER_VOLTAGE 93 | ] 94 | 95 | """ UNITS OF MEASUREMENT """ 96 | IMPERIAL_SYSTEM = 1 97 | METRIC_SYSTEM = 2 98 | 99 | # Degree units 100 | DEGREE = "°" 101 | 102 | # Temperature units 103 | TEMP_CELSIUS = f"{DEGREE}C" 104 | TEMP_FAHRENHEIT = f"{DEGREE}F" 105 | 106 | # Conductivity units 107 | CONDUCTIVITY = f"µS" 108 | 109 | # Percentage units 110 | PERCENTAGE = "%" 111 | 112 | 113 | # #### API / SOCKET #### 114 | SOCKET_PORT = 7002 115 | SPROUT_PORT = 7003 116 | WS_PORT = 7004 117 | SERVER_PORT = 8080 118 | 119 | URL_ROOT = "/" 120 | URL_API = "/api/" 121 | URL_API_CONFIG = "/api/config" 122 | 123 | HTTP_OK = 200 124 | HTTP_CREATED = 201 125 | HTTP_ACCEPTED = 202 126 | HTTP_MOVED_PERMANENTLY = 301 127 | HTTP_BAD_REQUEST = 400 128 | HTTP_UNAUTHORIZED = 401 129 | HTTP_FORBIDDEN = 403 130 | HTTP_NOT_FOUND = 404 131 | HTTP_METHOD_NOT_ALLOWED = 405 132 | HTTP_UNPROCESSABLE_ENTITY = 422 133 | HTTP_TOO_MANY_REQUESTS = 429 134 | HTTP_INTERNAL_SERVER_ERROR = 500 135 | HTTP_BAD_GATEWAY = 502 136 | HTTP_SERVICE_UNAVAILABLE = 503 137 | 138 | HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" 139 | 140 | CONTENT_TYPE_JSON = "application/json" 141 | CONTENT_TYPE_MULTIPART = "multipart/x-mixed-replace; boundary={}" 142 | CONTENT_TYPE_TEXT_PLAIN = "text/plain" 143 | 144 | -------------------------------------------------------------------------------- /mudpi/extensions/bme680/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | BME680 Sensor Interface 3 | Connects to a BME680 device to get 4 | environment and climate readings. 5 | """ 6 | import board 7 | import adafruit_bme680 8 | 9 | from busio import I2C 10 | from mudpi.extensions import BaseInterface 11 | from mudpi.extensions.sensor import Sensor 12 | from mudpi.exceptions import MudPiError, ConfigError 13 | 14 | 15 | class Interface(BaseInterface): 16 | 17 | def load(self, config): 18 | """ Load BME680 sensor component from configs """ 19 | sensor = BME680Sensor(self.mudpi, config) 20 | if sensor: 21 | self.add_component(sensor) 22 | return True 23 | 24 | def validate(self, config): 25 | """ Validate the bme680 config """ 26 | if not isinstance(config, list): 27 | config = [config] 28 | 29 | for conf in config: 30 | if not conf.get('key'): 31 | raise ConfigError('Missing `key` in i2c display config.') 32 | 33 | if not conf.get('address'): 34 | # raise ConfigError('Missing `address` in BME680 config.') 35 | conf['address'] = 0x77 36 | else: 37 | addr = conf['address'] 38 | 39 | # Convert hex string/int to actual hex 40 | if isinstance(addr, str): 41 | addr = int(addr, 16) 42 | 43 | conf['address'] = addr 44 | 45 | return config 46 | 47 | 48 | class BME680Sensor(Sensor): 49 | """ BME680 Sensor 50 | Gets readins for gas, pressure, humidity, 51 | temperature and altitude. 52 | """ 53 | 54 | """ Properties """ 55 | @property 56 | def id(self): 57 | """ Return a unique id for the component """ 58 | return self.config['key'] 59 | 60 | @property 61 | def name(self): 62 | """ Return the display name of the component """ 63 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 64 | 65 | @property 66 | def state(self): 67 | """ Return the state of the component (from memory, no IO!) """ 68 | return self._state 69 | 70 | @property 71 | def classifier(self): 72 | """ Classification further describing it, effects the data formatting """ 73 | return 'climate' 74 | 75 | 76 | """ Methods """ 77 | def init(self): 78 | """ Connect to the device """ 79 | self.i2c = I2C(board.SCL, board.SDA) 80 | self._sensor = adafruit_bme680.Adafruit_BME680_I2C( 81 | self.i2c, address=self.config['address'], debug=False 82 | ) 83 | # Change this to match the location's pressure (hPa) at sea level 84 | self._sensor.sea_level_pressure = self.config.get('calibration_pressure', 1013.25) 85 | 86 | return True 87 | 88 | def update(self): 89 | """ Get data from BME680 device""" 90 | temperature = round((self._sensor.temperature - 5) * 1.8 + 32, 2) 91 | gas = self._sensor.gas 92 | humidity = round(self._sensor.humidity, 1) 93 | pressure = round(self._sensor.pressure, 2) 94 | altitude = round(self._sensor.altitude, 3) 95 | 96 | if humidity is not None and temperature is not None: 97 | readings = { 98 | 'temperature': temperature, 99 | 'humidity': humidity, 100 | 'pressure': pressure, 101 | 'gas': gas, 102 | 'altitude': altitude 103 | } 104 | self._state = readings 105 | return readings 106 | else: 107 | Logger.log( 108 | LOG_LEVEL["error"], 109 | 'Failed to get reading [BME680]. Try again!' 110 | ) 111 | 112 | return None 113 | -------------------------------------------------------------------------------- /mudpi/events/__init__.py: -------------------------------------------------------------------------------- 1 | """ The core Event System for MudPi. 2 | 3 | Uses adaptors to provide events across 4 | different protocols for internal communications. 5 | 6 | Available Adaptors: 'mqtt', 'redis' 7 | Default: redis 8 | """ 9 | from uuid import uuid4 10 | from copy import deepcopy 11 | from mudpi.events import adaptors 12 | from mudpi.logger.Logger import Logger, LOG_LEVEL 13 | 14 | 15 | class EventSystem(): 16 | """ Main event manager that loads adaptors 17 | and coordinates the bus operations. """ 18 | 19 | def __init__(self, config={}): 20 | self.config = config 21 | self.prefix = config.get('prefix', 'mudpi_core_') 22 | self.topics = {} 23 | self.adaptors = {} 24 | self._load_adaptors() 25 | 26 | def connect(self): 27 | """ Setup connections for all adaptors """ 28 | connection_data = {} 29 | for key, adaptor in self.adaptors.items(): 30 | Logger.log_formatted( 31 | LOG_LEVEL["debug"], 32 | f"Preparing Event System for {key} ", 'Pending', 'notice' 33 | ) 34 | connection_data[key] = adaptor.connect() 35 | Logger.log_formatted( 36 | LOG_LEVEL["info"], 37 | f"Event System Ready on {key} ", 'Connected', 'success' 38 | ) 39 | return connection_data 40 | 41 | def disconnect(self): 42 | """ Disconnect all adaptors """ 43 | for key, adaptor in self.adaptors.items(): 44 | adaptor.disconnect() 45 | return True 46 | 47 | def subscribe(self, topic, callback): 48 | """ Add a subscriber to an event """ 49 | for key, adaptor in self.adaptors.items(): 50 | adaptor.subscribe(topic, callback) 51 | self.topics[key].append(topic) 52 | return True 53 | 54 | def unsubscribe(self, topic): 55 | """ Remove a subscriber from an event """ 56 | for key, adaptor in self.adaptors.items(): 57 | adaptor.unsubscribe(topic) 58 | self.topics[key].remove(topic) 59 | return True 60 | 61 | def publish(self, topic, data=None): 62 | """ Publish an event on an topic """ 63 | if data: 64 | if isinstance(data, dict): 65 | _data = deepcopy(data) 66 | if 'uuid' not in _data: 67 | _data['uuid'] = str(uuid4()) 68 | else: 69 | _data = data 70 | else: 71 | _data = data 72 | 73 | for key, adaptor in self.adaptors.items(): 74 | adaptor.publish(topic, _data) 75 | return True 76 | 77 | def subscribe_once(self, topic, callback): 78 | """ Listen to an event once """ 79 | for key, adaptor in self.adaptors.items(): 80 | adaptor.subscribe_once(topic, callback) 81 | return True 82 | 83 | def get_message(self): 84 | """ Request any new messages because some protocols 85 | require a poll for data """ 86 | for key, adaptor in self.adaptors.items(): 87 | adaptor.get_message() 88 | 89 | def events(self): 90 | """ Return all the events subscribed to [List] """ 91 | return self.topics 92 | 93 | 94 | def _load_adaptors(self): 95 | """ Load all the adaptors """ 96 | if self.config: 97 | for key, config in self.config.items(): 98 | if key in adaptors.Adaptor.adaptors: 99 | self.adaptors[key] = adaptors.Adaptor.adaptors[key](config) 100 | self.topics[key] = [] 101 | else: 102 | # Default adaptor 103 | self.adaptors['redis'] = adaptors.Adaptor.adaptors['redis']({"host": "127.0.0.1", "port": 6379}) 104 | self.topics['redis'] = [] -------------------------------------------------------------------------------- /mudpi/extensions/sensor/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sensor Trigger Interface 3 | Monitors sensor state changes and 4 | checks new state against any 5 | thresholds if provided. 6 | """ 7 | import json 8 | from mudpi.utils import decode_event_data 9 | from mudpi.exceptions import ConfigError 10 | from mudpi.extensions import BaseInterface 11 | from mudpi.extensions.trigger import Trigger 12 | from mudpi.logger.Logger import Logger, LOG_LEVEL 13 | 14 | 15 | class Interface(BaseInterface): 16 | 17 | def load(self, config): 18 | """ Load sensor Trigger component from configs """ 19 | trigger = SensorTrigger(self.mudpi, config) 20 | if trigger: 21 | self.add_component(trigger) 22 | return True 23 | 24 | def validate(self, config): 25 | """ Validate the trigger config """ 26 | if not isinstance(config, list): 27 | config = [config] 28 | 29 | for conf in config: 30 | if not conf.get('source'): 31 | raise ConfigError('Missing `source` key in Sensor Trigger config.') 32 | 33 | return config 34 | 35 | 36 | class SensorTrigger(Trigger): 37 | """ A trigger that listens to sensor 38 | states and checks for new state that 39 | matches any thresholds. 40 | """ 41 | 42 | 43 | """ Methods """ 44 | def init(self): 45 | """ Listen to the sensors state for changes """ 46 | super().init() 47 | 48 | # Used for onetime subscribe 49 | self._listening = False 50 | 51 | if self.mudpi.is_prepared: 52 | if not self._listening: 53 | # TODO: Eventually get a handler returned to unsub just this listener 54 | self.mudpi.events.subscribe('state', self.handle_event) 55 | self._listening = True 56 | return True 57 | 58 | def handle_event(self, event): 59 | """ Handle the event data from the event system """ 60 | _event_data = decode_event_data(event) 61 | 62 | if _event_data == self._last_event: 63 | # Event already handled 64 | return 65 | 66 | self._last_event = _event_data 67 | if _event_data.get('event'): 68 | try: 69 | if _event_data['event'] == 'StateUpdated': 70 | if _event_data['component_id'] == self.source: 71 | sensor_value = self._parse_data(_event_data["new_state"]["state"]) 72 | if self.evaluate_thresholds(sensor_value): 73 | self.active = True 74 | if self._previous_state != self.active: 75 | # Trigger is reset, Fire 76 | self.trigger(_event_data) 77 | else: 78 | # Trigger not reset check if its multi fire 79 | if self.frequency == 'many': 80 | self.trigger(_event_data) 81 | else: 82 | self.active = False 83 | except Exception as error: 84 | Logger.log(LOG_LEVEL["error"], 85 | f'Error evaluating thresholds for trigger {self.id}') 86 | Logger.log(LOG_LEVEL["debug"], error) 87 | self._previous_state = self.active 88 | 89 | def unload(self): 90 | # Unsubscribe once bus supports single handler unsubscribes 91 | return 92 | 93 | def _parse_data(self, data): 94 | """ Get nested data if set otherwise return the data """ 95 | try: 96 | data = json.loads(data) 97 | except Exception as error: 98 | pass 99 | if isinstance(data, dict): 100 | return data if not self.nested_source else data.get(self.nested_source, None) 101 | return data 102 | 103 | -------------------------------------------------------------------------------- /mudpi/extensions/action/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Actions Extension 3 | Enables MudPi to perfrom operations in response to a trigger. 4 | Components expose their own actions however you can also make 5 | actions manually through configs for more custom interactions. 6 | """ 7 | import json 8 | import subprocess 9 | from mudpi.extensions import Component, BaseExtension 10 | 11 | 12 | NAMESPACE = 'action' 13 | UPDATE_INTERVAL = 30 14 | 15 | class Extension(BaseExtension): 16 | namespace = NAMESPACE 17 | update_interval = UPDATE_INTERVAL 18 | 19 | def init(self, config): 20 | self.config = config[self.namespace] 21 | 22 | for entry in self.config: 23 | action = Action(self.mudpi, entry) 24 | self.mudpi.actions.register(action.id, action) 25 | return True 26 | 27 | def validate(self, config): 28 | """ Custom Validation for Action configs 29 | - Requires `key` 30 | """ 31 | key = None 32 | for item in config[self.namespace]: 33 | try: 34 | key = item.get("key") 35 | except Exception as error: 36 | key = None 37 | 38 | if key is None: 39 | raise ConfigError("Missing `key` in configs.") 40 | return config 41 | 42 | 43 | """ Action Templates """ 44 | class Action: 45 | """ Actions perfrom operations in response to a trigger. 46 | 47 | Can be called by MudPi typically with a trigger to 48 | emit an event, run a command, or query a service. 49 | """ 50 | 51 | def __init__(self, mudpi, config): 52 | self.mudpi = mudpi 53 | self.config = config 54 | 55 | self.init() 56 | 57 | def init(self): 58 | """ Action will be different depending on type """ 59 | # Event: json object 60 | # Command: command string 61 | if self.type == 'event': 62 | self.topic = self.config.get("topic", "mudpi") 63 | elif self.type == 'command': 64 | self.shell = self.config.get("shell", False) 65 | 66 | return True 67 | 68 | """ Properties """ 69 | @property 70 | def name(self): 71 | """ Return a friendly name for the Action """ 72 | return self.config.get("name", f"Action-{self.id}") 73 | 74 | @property 75 | def id(self): 76 | """ Returns a unique id for the Action """ 77 | return self.config.get("key", None).replace(" ", "_").lower() if self.config.get( 78 | "key") is not None else self.name.replace(" ", "_").lower() 79 | 80 | """ Custom Properties """ 81 | @property 82 | def type(self): 83 | """ Returns the type of action. (Event or Command) """ 84 | return self.config.get("type", "event") 85 | 86 | @property 87 | def action(self): 88 | """ Returns the action to take """ 89 | return self.config.get("action", None) 90 | 91 | """ Methods """ 92 | def trigger(self, value=None): 93 | """ Trigger the action """ 94 | if self.type == 'event': 95 | self._emit_event() 96 | elif self.type == 'command': 97 | self._run_command(value) 98 | return 99 | 100 | 101 | """ Internal Methods """ 102 | def _emit_event(self): 103 | """ Emit an event """ 104 | self.mudpi.events.publish(self.topic, self.action) 105 | return 106 | 107 | def _run_command(self, value=None): 108 | """ Run the command """ 109 | if value is None: 110 | completed_process = subprocess.run([self.action], shell=self.shell) 111 | else: 112 | completed_process = subprocess.run( 113 | [self.action, json.dumps(value)], shell=self.shell) 114 | return 115 | 116 | def __call__(self, val=None): 117 | """ Trigger the action when it is called """ 118 | return self.trigger(val) -------------------------------------------------------------------------------- /mudpi/extensions/state/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | State Trigger Interface 3 | Monitors state changes and 4 | checks new state against any 5 | thresholds if provided. 6 | """ 7 | import json 8 | from mudpi.utils import decode_event_data 9 | from mudpi.exceptions import ConfigError 10 | from mudpi.extensions import BaseInterface 11 | from mudpi.extensions.trigger import Trigger 12 | from mudpi.logger.Logger import Logger, LOG_LEVEL 13 | 14 | 15 | class Interface(BaseInterface): 16 | 17 | def load(self, config): 18 | """ Load state Trigger component from configs """ 19 | trigger = StateTrigger(self.mudpi, config) 20 | if trigger: 21 | self.add_component(trigger) 22 | return True 23 | 24 | def validate(self, config): 25 | """ Validate the trigger config """ 26 | if not isinstance(config, list): 27 | config = [config] 28 | 29 | for conf in config: 30 | if not conf.get('source'): 31 | raise ConfigError('Missing `source` key in Sensor Trigger config.') 32 | 33 | return config 34 | 35 | 36 | class StateTrigger(Trigger): 37 | """ A trigger that listens to states 38 | and checks for new state that 39 | matches any thresholds. 40 | """ 41 | 42 | # Used for onetime subscribe 43 | _listening = False 44 | 45 | 46 | """ Methods """ 47 | def init(self): 48 | """ Listen to the state for changes """ 49 | super().init() 50 | 51 | # Used for onetime subscribe 52 | self._listening = False 53 | 54 | if self.mudpi.is_prepared: 55 | if not self._listening: 56 | # TODO: Eventually get a handler returned to unsub just this listener 57 | self.mudpi.events.subscribe('state', self.handle_event) 58 | self._listening = True 59 | return True 60 | 61 | def handle_event(self, event): 62 | """ Handle the event data from the event system """ 63 | _event_data = decode_event_data(event) 64 | 65 | if _event_data == self._last_event: 66 | # Event already handled 67 | return 68 | 69 | self._last_event = _event_data 70 | if _event_data.get('event'): 71 | try: 72 | if _event_data['event'] == 'StateUpdated': 73 | if _event_data['component_id'] == self.source: 74 | sensor_value = self._parse_data(_event_data["new_state"]["state"]) 75 | if self.evaluate_thresholds(sensor_value): 76 | self.active = True 77 | if self._previous_state != self.active: 78 | # Trigger is reset, Fire 79 | self.trigger(_event_data) 80 | else: 81 | # Trigger not reset check if its multi fire 82 | if self.frequency == 'many': 83 | self.trigger(_event_data) 84 | else: 85 | self.active = False 86 | except Exception as error: 87 | Logger.log(LOG_LEVEL["error"], 88 | f'Error evaluating thresholds for trigger {self.id}') 89 | Logger.log(LOG_LEVEL["debug"], error) 90 | self._previous_state = self.active 91 | 92 | def unload(self): 93 | # Unsubscribe once bus supports single handler unsubscribes 94 | return 95 | 96 | def _parse_data(self, data): 97 | """ Get nested data if set otherwise return the data """ 98 | try: 99 | data = json.loads(data) 100 | except Exception as error: 101 | pass 102 | if isinstance(data, dict): 103 | return data if not self.nested_source else data.get(self.nested_source, None) 104 | return data 105 | -------------------------------------------------------------------------------- /mudpi/extensions/gpio/toggle.py: -------------------------------------------------------------------------------- 1 | """ 2 | GPIO Toggle Interface 3 | Connects to a linux board GPIO to 4 | toggle output on and off. Useful for 5 | turning things on like lights or pumps. 6 | """ 7 | import re 8 | import time 9 | import board 10 | import digitalio 11 | from mudpi.extensions import BaseInterface 12 | from mudpi.extensions.toggle import Toggle 13 | from mudpi.exceptions import MudPiError, ConfigError 14 | 15 | 16 | class Interface(BaseInterface): 17 | 18 | def load(self, config): 19 | """ Load GPIO toggle component from configs """ 20 | toggle = GPIOToggle(self.mudpi, config) 21 | if toggle: 22 | self.add_component(toggle) 23 | return True 24 | 25 | def validate(self, config): 26 | """ Validate the dht config """ 27 | if not isinstance(config, list): 28 | config = [config] 29 | 30 | for conf in config: 31 | if conf.get('key') is None: 32 | raise ConfigError('Missing `key` in GPIO toggle config.') 33 | 34 | if conf.get('pin') is None: 35 | raise ConfigError('Missing `pin` in GPIO toggle config.') 36 | 37 | conf['pin'] = str(conf['pin']) 38 | 39 | return config 40 | 41 | 42 | class GPIOToggle(Toggle): 43 | """ GPIO Toggle 44 | Turns a GPIO pin on or off 45 | """ 46 | 47 | """ Properties """ 48 | @property 49 | def pin(self): 50 | """ The GPIO pin """ 51 | return self.config.get('pin') 52 | 53 | 54 | """ Methods """ 55 | def init(self): 56 | """ Connect to the device """ 57 | self.pin_obj = getattr(board, self.pin) 58 | 59 | if re.match(r'D\d+$', self.pin): 60 | self.is_digital = True 61 | elif re.match(r'A\d+$', self.pin): 62 | self.is_digital = False 63 | else: 64 | self.is_digital = True 65 | 66 | if self.invert_state: 67 | self.pin_state_on = False 68 | self.pin_state_off = True 69 | else: 70 | self.pin_state_on = True 71 | self.pin_state_off = False 72 | 73 | self.gpio = digitalio 74 | self.gpio_pin = digitalio.DigitalInOut(self.pin_obj) 75 | self.gpio_pin.switch_to_output() 76 | 77 | self.gpio_pin.value = self.pin_state_off 78 | # Active is used to keep track of durations 79 | self.active = False 80 | time.sleep(0.1) 81 | 82 | return True 83 | 84 | def restore_state(self, state): 85 | """ This is called on start to 86 | restore previous state """ 87 | self.active = state.state 88 | self.gpio_pin.value = self.pin_state_on if self.active else self.pin_state_off 89 | self.reset_duration() 90 | return 91 | 92 | 93 | """ Actions """ 94 | def toggle(self, data={}): 95 | # Toggle the GPIO state 96 | if self.mudpi.is_prepared: 97 | # Do inverted check and change value before setting active 98 | # to avoid false state being provided in the event fired. 99 | self.active = not self.active 100 | self.gpio_pin.value = self.pin_state_on if self.active else self.pin_state_off 101 | self.store_state() 102 | self.fire() 103 | 104 | def turn_on(self, data={}): 105 | # Turn on GPIO if its not on 106 | if self.mudpi.is_prepared: 107 | if not self.active: 108 | self.gpio_pin.value = self.pin_state_on 109 | self.active = True 110 | self.store_state() 111 | self.fire() 112 | 113 | def turn_off(self, data={}): 114 | # Turn off GPIO if its not off 115 | if self.mudpi.is_prepared: 116 | if self.active: 117 | self.gpio_pin.value = self.pin_state_off 118 | self.active = False 119 | self.store_state() 120 | self.fire() -------------------------------------------------------------------------------- /mudpi/extensions/t9602/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | T9602 Sensor Interface 3 | Connects to a T9602 device to get 4 | environment and climate readings. 5 | """ 6 | import smbus 7 | import time 8 | 9 | from mudpi.constants import METRIC_SYSTEM 10 | from mudpi.extensions import BaseInterface 11 | from mudpi.extensions.sensor import Sensor 12 | from mudpi.logger.Logger import Logger, LOG_LEVEL 13 | from mudpi.exceptions import MudPiError, ConfigError 14 | 15 | 16 | class Interface(BaseInterface): 17 | 18 | def load(self, config): 19 | """ Load T9602 sensor component from configs """ 20 | sensor = T9602Sensor(self.mudpi, config) 21 | if sensor.connect(): 22 | self.add_component(sensor) 23 | return True 24 | 25 | def validate(self, config): 26 | """ Validate the T9602 config """ 27 | if not isinstance(config, list): 28 | config = [config] 29 | 30 | for conf in config: 31 | if not conf.get('address'): 32 | # raise ConfigError('Missing `address` in T9602 config.') 33 | conf['address'] = 0x28 34 | else: 35 | addr = conf['address'] 36 | 37 | if isinstance(addr, str): 38 | addr = int(addr, 16) 39 | 40 | conf['address'] = addr 41 | 42 | return config 43 | 44 | 45 | 46 | class T9602Sensor(Sensor): 47 | """ T9602 Sensor 48 | Get readings for humidity and temperature. 49 | """ 50 | 51 | """ Properties """ 52 | @property 53 | def id(self): 54 | """ Return a unique id for the component """ 55 | return self.config['key'] 56 | 57 | @property 58 | def name(self): 59 | """ Return the display name of the component """ 60 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 61 | 62 | @property 63 | def state(self): 64 | """ Return the state of the component (from memory, no IO!) """ 65 | return self._state 66 | 67 | @property 68 | def classifier(self): 69 | """ Classification further describing it, effects the data formatting """ 70 | return 'climate' 71 | 72 | 73 | """ Methods """ 74 | def connect(self): 75 | """ Connect to the Device 76 | This is the bus number : the 1 in "/dev/i2c-1" 77 | I enforced it to 1 because there is only one on Raspberry Pi. 78 | We might want to add this parameter in i2c sensor config in the future. 79 | We might encounter boards with several buses.""" 80 | self.bus = smbus.SMBus(1) 81 | 82 | return True 83 | 84 | def update(self): 85 | """ Get data from T9602 device""" 86 | for trynb in range(5): # 5 tries 87 | try: 88 | data = self.bus.read_i2c_block_data(self.config['address'], 0, 4) 89 | break 90 | except OSError: 91 | Logger.log( 92 | LOG_LEVEL["info"], 93 | "Single reading error [t9602]. It happens, let's try again..." 94 | ) 95 | time.sleep(2) 96 | 97 | 98 | humidity = (((data[0] & 0x3F) << 8) + data[1]) / 16384.0 * 100.0 99 | temperature_c = ((data[2] * 64) + (data[3] >> 2)) / 16384.0 * 165.0 - 40.0 100 | 101 | humidity = round(humidity, 2) 102 | temperature_c = round(temperature_c, 2) 103 | 104 | if humidity is not None and temperature_c is not None: 105 | _temperature = temperature_c if self.mudpi.unit_system == METRIC_SYSTEM else (temperature_c * 1.8 + 32) 106 | readings = { 107 | 'temperature': _temperature, 108 | 'humidity': humidity 109 | } 110 | self._state = readings 111 | return readings 112 | else: 113 | Logger.log( 114 | LOG_LEVEL["error"], 115 | 'Failed to get reading [t9602]. Try again!' 116 | ) 117 | return None 118 | -------------------------------------------------------------------------------- /mudpi/extensions/control/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Control Trigger Interface 3 | Monitors control state changes and 4 | checks new state against any 5 | thresholds if provided. 6 | """ 7 | from . import NAMESPACE 8 | from mudpi.utils import decode_event_data 9 | from mudpi.exceptions import ConfigError 10 | from mudpi.extensions import BaseInterface 11 | from mudpi.extensions.trigger import Trigger 12 | from mudpi.logger.Logger import Logger, LOG_LEVEL 13 | 14 | 15 | class Interface(BaseInterface): 16 | 17 | def load(self, config): 18 | """ Load control Trigger component from configs """ 19 | trigger = ControlTrigger(self.mudpi, config) 20 | if trigger: 21 | self.add_component(trigger) 22 | return True 23 | 24 | def validate(self, config): 25 | """ Validate the trigger config """ 26 | if not isinstance(config, list): 27 | config = [config] 28 | 29 | for conf in config: 30 | if not conf.get('source'): 31 | raise ConfigError('Missing `source` key in Sensor Trigger config.') 32 | 33 | return config 34 | 35 | 36 | class ControlTrigger(Trigger): 37 | """ A trigger that listens to states 38 | and checks for new state that 39 | matches any thresholds. 40 | """ 41 | 42 | # Used for onetime subscribe 43 | _listening = False 44 | 45 | # Type of events to listen to 46 | _events = { 47 | 'default': "ControlUpdated", 48 | 'pressed': "ControlPressed", 49 | 'released': "ControlReleased" 50 | } 51 | 52 | @property 53 | def type(self): 54 | """ Return Trigger Type """ 55 | return self.config.get("type", "default") 56 | 57 | 58 | def init(self): 59 | """ Listen to the state for changes """ 60 | super().init() 61 | if self.mudpi.is_prepared: 62 | if not self._listening: 63 | # TODO: Eventually get a handler returned to unsub just this listener 64 | self.mudpi.events.subscribe(NAMESPACE, self.handle_event) 65 | self._listening = True 66 | return True 67 | 68 | """ Methods """ 69 | def handle_event(self, event): 70 | """ Handle the event data from the event system """ 71 | _event_data = decode_event_data(event) 72 | 73 | if _event_data == self._last_event: 74 | # Event already handled 75 | return 76 | 77 | self._last_event = _event_data 78 | if _event_data.get('event'): 79 | try: 80 | if _event_data['event'] == self._events[self.type]: 81 | if _event_data['component_id'] == self.source: 82 | _value = self._parse_data(_event_data["state"]) 83 | if self.evaluate_thresholds(_value): 84 | self.active = True 85 | if self._previous_state != self.active: 86 | # Trigger is reset, Fire 87 | self.trigger(_event_data) 88 | else: 89 | # Trigger not reset check if its multi fire 90 | if self.frequency == 'many': 91 | self.trigger(_event_data) 92 | else: 93 | self.active = False 94 | except Exception as error: 95 | Logger.log(LOG_LEVEL["error"], 96 | f'Error evaluating thresholds for trigger {self.id}') 97 | Logger.log(LOG_LEVEL["debug"], error) 98 | self._previous_state = self.active 99 | 100 | def unload(self): 101 | # Unsubscribe once bus supports single handler unsubscribes 102 | return 103 | 104 | def _parse_data(self, data): 105 | """ Get nested data if set otherwise return the data """ 106 | if isinstance(data, dict): 107 | return data if not self.nested_source else data.get(self.nested_source, None) 108 | return data 109 | 110 | -------------------------------------------------------------------------------- /mudpi/extensions/nfc/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | NFC Trigger Interface 3 | Monitors NFC for scans and 4 | listens to specific NFC events. 5 | """ 6 | from . import NAMESPACE 7 | from mudpi.utils import decode_event_data 8 | from mudpi.exceptions import ConfigError 9 | from mudpi.extensions import BaseInterface 10 | from mudpi.extensions.trigger import Trigger 11 | from mudpi.logger.Logger import Logger, LOG_LEVEL 12 | 13 | 14 | class Interface(BaseInterface): 15 | 16 | def load(self, config): 17 | """ Load Trigger component from configs """ 18 | trigger = NFCTrigger(self.mudpi, config) 19 | if trigger: 20 | self.add_component(trigger) 21 | return True 22 | 23 | def validate(self, config): 24 | """ Validate the trigger config """ 25 | if not isinstance(config, list): 26 | config = [config] 27 | 28 | for conf in config: 29 | if not conf.get('source'): 30 | # raise ConfigError('Missing `source` key in NFC Trigger config.') 31 | pass 32 | 33 | return config 34 | 35 | 36 | class NFCTrigger(Trigger): 37 | """ A trigger that listens to states 38 | and checks for new state that 39 | matches any thresholds. 40 | """ 41 | 42 | # Used for onetime subscribe 43 | _listening = False 44 | 45 | # Type of events to listen to 46 | _events = { 47 | 'tag_scanned': "NFCTagScanned", 48 | 'new_tag': "NFCNewTagScanned" 49 | 'removed': "NFCTagRemoved" 50 | } 51 | 52 | @property 53 | def type(self): 54 | """ Return Trigger Type """ 55 | return self.config.get("type", "tag_scanned") 56 | 57 | 58 | def init(self): 59 | """ Listen to the state for changes """ 60 | super().init() 61 | if self.mudpi.is_prepared: 62 | if not self._listening: 63 | # TODO: Eventually get a handler returned to unsub just this listener 64 | self.mudpi.events.subscribe(NAMESPACE, self.handle_event) 65 | self._listening = True 66 | return True 67 | 68 | """ Methods """ 69 | def handle_event(self, event): 70 | """ Handle the event data from the event system """ 71 | _event_data = decode_event_data(event) 72 | 73 | if _event_data == self._last_event: 74 | # Event already handled 75 | return 76 | 77 | self._last_event = _event_data 78 | if _event_data.get('event'): 79 | try: 80 | if _event_data['event'] == self._events[self.type]: 81 | if _event_data['tag_id'] == self.source or _event_data['key'] == self.source: 82 | _value = self._parse_data(_event_data) 83 | if self.evaluate_thresholds(_value): 84 | self.active = True 85 | if self._previous_state != self.active: 86 | # Trigger is reset, Fire 87 | self.trigger(_event_data) 88 | else: 89 | # Trigger not reset check if its multi fire 90 | if self.frequency == 'many': 91 | self.trigger(_event_data) 92 | else: 93 | self.active = False 94 | except Exception as error: 95 | Logger.log(LOG_LEVEL["error"], 96 | f'Error evaluating thresholds for trigger {self.id}') 97 | Logger.log(LOG_LEVEL["debug"], error) 98 | self._previous_state = self.active 99 | 100 | def unload(self): 101 | # Unsubscribe once bus supports single handler unsubscribes 102 | return 103 | 104 | def _parse_data(self, data): 105 | """ Get nested data if set otherwise return the data """ 106 | if isinstance(data, dict): 107 | return data.get('tag_id') if not self.nested_source else data.get(self.nested_source, None) 108 | return data 109 | 110 | -------------------------------------------------------------------------------- /mudpi/extensions/i2c/char_display.py: -------------------------------------------------------------------------------- 1 | """ 2 | Character Display Interface 3 | Connects to a LCD character 4 | display through a linux I2C. 5 | """ 6 | import board 7 | import busio 8 | from mudpi.extensions import BaseInterface 9 | from mudpi.logger.Logger import Logger, LOG_LEVEL 10 | from mudpi.extensions.char_display import CharDisplay 11 | import adafruit_character_lcd.character_lcd_rgb_i2c as character_rgb_lcd 12 | import adafruit_character_lcd.character_lcd_i2c as character_lcd 13 | 14 | 15 | class Interface(BaseInterface): 16 | 17 | def load(self, config): 18 | """ Load display component from configs """ 19 | display = I2CCharDisplay(self.mudpi, config) 20 | if display: 21 | self.add_component(display) 22 | return True 23 | 24 | 25 | def validate(self, config): 26 | """ Validate the display configs """ 27 | if not isinstance(config, list): 28 | config = [config] 29 | 30 | for conf in config: 31 | if not conf.get('key'): 32 | raise ConfigError('Missing `key` in i2c display config.') 33 | 34 | if not conf.get('address'): 35 | # raise ConfigError('Missing `address` in i2c char_lcd config.') 36 | conf['address'] = 0x27 37 | else: 38 | addr = conf['address'] 39 | 40 | # Convert hex string/int to actual hex 41 | if isinstance(addr, str): 42 | addr = int(addr, 16) 43 | 44 | conf['address'] = addr 45 | 46 | if not conf.get('columns', 16): 47 | raise ConfigError('Missing `columns` must be an int.') 48 | 49 | if not conf.get('rows', 2): 50 | raise ConfigError('Missing `rows` must be an int.') 51 | 52 | return config 53 | 54 | 55 | class I2CCharDisplay(CharDisplay): 56 | """ I2C Character Display 57 | Displays messages through i2c lcd. 58 | """ 59 | 60 | @property 61 | def address(self): 62 | """ Unique id or key """ 63 | return self.config.get('address', 0x27) 64 | 65 | @property 66 | def model(self): 67 | """ Return the model (rgb, i2c, pcf) """ 68 | if self.config.get('model', 'i2c') not in ('rgb', 'i2c', 'pcf'): 69 | self.config['model'] = 'i2c' 70 | return self.config.get('model', 'i2c').lower() 71 | 72 | 73 | """ Actions """ 74 | def clear(self, data=None): 75 | """ Clear the display screen """ 76 | self.lcd.clear() 77 | 78 | def show(self, data={}): 79 | """ Show a message on the display """ 80 | if not isinstance(data, dict): 81 | data = {'message': data} 82 | 83 | self.lcd.message = data.get('message', '') 84 | 85 | def turn_on_backlight(self): 86 | """ Turn the backlight on """ 87 | self.lcd.backlight = True 88 | 89 | def turn_off_backlight(self): 90 | """ Turn the backlight on """ 91 | self.lcd.backlight = False 92 | 93 | 94 | """ Methods """ 95 | def init(self): 96 | """ Connect to the display over I2C """ 97 | super().init() 98 | 99 | # Prepare the display i2c connection 100 | self.i2c = busio.I2C(board.SCL, board.SDA) 101 | 102 | if (self.model == 'rgb'): 103 | self.lcd = character_lcd.Character_LCD_RGB_I2C( 104 | self.i2c, 105 | self.columns, 106 | self.rows, 107 | self.address 108 | ) 109 | elif (self.model == 'pcf'): 110 | self.lcd = character_lcd.Character_LCD_I2C( 111 | self.i2c, 112 | self.columns, 113 | self.rows, 114 | address=self.address, 115 | usingPCF=True 116 | ) 117 | else: 118 | self.lcd = character_lcd.Character_LCD_I2C( 119 | self.i2c, 120 | self.columns, 121 | self.rows, 122 | self.address 123 | ) 124 | 125 | self.turn_on_backlight() 126 | self.clear() 127 | -------------------------------------------------------------------------------- /mudpi/server/mudpi_server.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import json 4 | import time 5 | import redis 6 | import socket 7 | import threading 8 | 9 | from logger.Logger import Logger, LOG_LEVEL 10 | 11 | 12 | class MudpiServer(object): 13 | """ 14 | A socket server used to allow incoming wiresless connections. 15 | MudPi will listen on the socket server for clients to join and 16 | send a message that should be broadcast on the event system. 17 | """ 18 | 19 | def __init__(self, config, system_running): 20 | self.port = int(config.get("port", 7007)) 21 | self.host = config.get("host", "127.0.0.1") 22 | self.system_running = system_running 23 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 24 | self.client_threads = [] 25 | 26 | 27 | try: 28 | self.sock.bind((self.host, self.port)) 29 | except socket.error as msg: 30 | Logger.log(LOG_LEVEL['error'], 'Failed to create socket. Error Code: ', str(msg[0]), ' , Error Message: ', msg[1]) 31 | sys.exit() 32 | 33 | # PubSub 34 | try: 35 | self.r = config["redis"] 36 | except KeyError: 37 | self.r = redis.Redis(host='127.0.0.1', port=6379) 38 | 39 | def listen(self): 40 | self.sock.listen(0) # number of clients to listen for. 41 | Logger.log(LOG_LEVEL['info'], 'MudPi Server...\t\t\t\t\033[1;32m Online\033[0;0m ') 42 | while self.system_running.is_set(): 43 | try: 44 | client, address = self.sock.accept() 45 | client.settimeout(600) 46 | ip, port = client.getpeername() 47 | Logger.log(LOG_LEVEL['info'], 'Socket \033[1;32mClient {0}\033[0;0m from \033[1;32m{1} Connected\033[0;0m'.format(port, ip)) 48 | t = threading.Thread(target = self.listenToClient, args = (client, address, ip)) 49 | self.client_threads.append(t) 50 | t.start() 51 | except Exception as e: 52 | Logger.log(LOG_LEVEL['error'], e) 53 | time.sleep(1) 54 | pass 55 | self.sock.close() 56 | if len(self.client_threads > 0): 57 | for client in self.client_threads: 58 | client.join() 59 | Logger.log(LOG_LEVEL['info'], 'Server Shutdown...\t\t\t\033[1;32m Complete\033[0;0m') 60 | 61 | def listenToClient(self, client, address, ip): 62 | size = 1024 63 | while self.system_running.is_set(): 64 | try: 65 | data = client.recv(size) 66 | if data: 67 | data = self.decodeMessageData(data) 68 | if data.get("topic", None) is not None: 69 | self.r.publish(data["topic"], json.dumps(data)) 70 | Logger.log(LOG_LEVEL['info'], "Socket Event \033[1;36m{event}\033[0;0m from \033[1;36m{source}\033[0;0m Dispatched".format(**data)) 71 | 72 | # response = { 73 | # "status": "OK", 74 | # "code": 200 75 | # } 76 | # client.send(json.dumps(response).encode('utf-8')) 77 | else: 78 | Logger.log(LOG_LEVEL['error'], "Socket Data Recieved. \033[1;31mDispatch Failed:\033[0;0m Missing Data 'Topic'") 79 | Logger.log(LOG_LEVEL['debug'], data) 80 | else: 81 | pass 82 | # raise error('Client Disconnected') 83 | except Exception as e: 84 | Logger.log(LOG_LEVEL['info'], "Socket Client \033[1;31m{0} Disconnected\033[0;0m".format(ip)) 85 | client.close() 86 | return False 87 | Logger.log(LOG_LEVEL['info'], 'Closing Client Connection...\t\t\033[1;32m Complete\033[0;0m') 88 | 89 | def decodeMessageData(self, message): 90 | if isinstance(message, dict): 91 | return message # print('Dict Found') 92 | elif isinstance(message.decode('utf-8'), str): 93 | try: 94 | temp = json.loads(message.decode('utf-8')) 95 | return temp # print('Json Found') 96 | except: 97 | return {'event':'Unknown', 'data':message.decode('utf-8')} # print('Json Error. Str Found') 98 | else: 99 | return {'event':'Unknown', 'data':message} # print('Failed to detect type') 100 | 101 | if __name__ == "__main__": 102 | config = { 103 | "host": '', 104 | "port": 7007 105 | } 106 | system_ready = threading.Event() 107 | system_ready.set() 108 | server = MudpiServer(config, system_ready) 109 | server.listen() 110 | try: 111 | while system_ready.is_set(): 112 | time.sleep(1) 113 | except KeyboardInterrupt: 114 | system_ready.clear() 115 | finally: 116 | system_ready.clear() 117 | -------------------------------------------------------------------------------- /mudpi/extensions/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MQTT Extension 3 | Includes interfaces for redis to 4 | get data from events. 5 | """ 6 | import time 7 | import paho.mqtt.client as mqtt 8 | from mudpi.extensions import BaseExtension 9 | 10 | 11 | class Extension(BaseExtension): 12 | namespace = 'mqtt' 13 | update_interval = 1 14 | 15 | def init(self, config): 16 | """ Prepare the mqtt connection and components """ 17 | self.connections = {} 18 | self.loop_started = False 19 | 20 | self.config = config 21 | 22 | if not isinstance(config, list): 23 | config = [config] 24 | 25 | # Prepare clients for mqtt 26 | for conf in config: 27 | host = conf.get('host', 'localhost') 28 | port = conf.get('port', 1883) 29 | if conf['key'] not in self.connections: 30 | self.connections[conf['key']] = {'client': None, 31 | 'connected': False, 32 | 'loop_started': False, 33 | 'callbacks': {}} 34 | 35 | def on_conn(client, userdata, flags, rc): 36 | if rc == 0: 37 | self.connections[conf['key']]['connected'] = True 38 | 39 | self.connections[conf['key']]['client'] = mqtt.Client(f'mudpi-{conf["key"]}') 40 | self.connections[conf['key']]['client'].on_connect = on_conn 41 | username = conf.get('username') 42 | password = conf.get('password') 43 | if all([username, password]): 44 | self.connections[conf['key']]['client'].username_pw_set(username, password) 45 | self.connections[conf['key']]['client'].connect(host, port=port) 46 | 47 | while not self.connections[conf['key']]['connected']: 48 | if not self.connections[conf['key']]['loop_started']: 49 | self.connections[conf['key']]['client'].loop_start() 50 | self.connections[conf['key']]['loop_started'] = True 51 | time.sleep(0.1) 52 | 53 | return True 54 | 55 | def validate(self, config): 56 | """ Validate the mqtt connection info """ 57 | config = config[self.namespace] 58 | if not isinstance(config, list): 59 | config = [config] 60 | 61 | for conf in config: 62 | key = conf.get('key') 63 | if key is None: 64 | raise ConfigError('MQTT missing a `key` in config for connection') 65 | 66 | host = conf.get('host') 67 | if host is None: 68 | conf['host'] = 'localhost' 69 | 70 | port = conf.get('port') 71 | if port is None: 72 | conf['port'] = 1883 73 | 74 | username = conf.get('username') 75 | password = conf.get('password') 76 | if any([username, password]) and not all([username, password]): 77 | raise ConfigError('A username and password must both be provided.') 78 | 79 | 80 | return config 81 | 82 | def unload(self): 83 | """ Unload the extension """ 84 | for conn in self.connections.values(): 85 | conn['client'].loop_stop() 86 | conn['client'].disconnect() 87 | 88 | def subscribe(self, key, topic, callback): 89 | """ Listen on a topic and pass event data to callback """ 90 | if topic not in self.connections[key]['callbacks']: 91 | self.connections[key]['callbacks'][topic] = [callback] 92 | else: 93 | if callback not in self.connections[key]['callbacks'][topic]: 94 | self.connections[key]['callbacks'][topic].append(callback) 95 | 96 | def callback_handler(client, userdata, message): 97 | # log = f"{message.payload.decode()} {message.topic}" 98 | if message.topic in self.connections[key]['callbacks']: 99 | for callbk in self.connections[key]['callbacks'][message.topic]: 100 | callbk(message.payload.decode("utf-8")) 101 | 102 | self.connections[key]['client'].on_message = callback_handler 103 | return self.connections[key]['client'].subscribe(topic) -------------------------------------------------------------------------------- /mudpi/extensions/i2c/toggle.py: -------------------------------------------------------------------------------- 1 | """ 2 | I2C Toggle Interface 3 | Connects to a linux I2C bus to 4 | toggle output on and off. Useful for 5 | turning things on like lights or pumps. 6 | """ 7 | import smbus2 8 | import time 9 | from mudpi.extensions import BaseInterface 10 | from mudpi.extensions.toggle import Toggle 11 | from mudpi.exceptions import MudPiError, ConfigError 12 | 13 | 14 | DEVICE_BUS = 1 15 | DEVICE_ADDR = 0x10 16 | 17 | 18 | class Interface(BaseInterface): 19 | 20 | def load(self, config): 21 | """ Load I2C toggle component from configs """ 22 | toggle = I2CToggle(self.mudpi, config) 23 | if toggle: 24 | self.add_component(toggle) 25 | return True 26 | 27 | def validate(self, config): 28 | """ Validate the I2C config """ 29 | if not isinstance(config, list): 30 | config = [config] 31 | 32 | for conf in config: 33 | if conf.get('key') is None: 34 | raise ConfigError('Missing `key` in i2c toggle config.') 35 | 36 | if not conf.get('address'): 37 | # raise ConfigError('Missing `address` in i2c toggle config.') 38 | conf['address'] = DEVICE_ADDR 39 | else: 40 | addr = conf['address'] 41 | 42 | # Convert hex string/int to actual hex 43 | if isinstance(addr, str): 44 | addr = int(addr, 16) 45 | 46 | conf['address'] = addr 47 | 48 | if not conf.get('register'): 49 | # raise ConfigError('Missing `address` in i2c toggle config.') 50 | conf['register'] = 0x01 51 | else: 52 | reg = conf['register'] 53 | 54 | # Convert hex string/int to actual hex 55 | if isinstance(reg, str): 56 | reg = int(reg, 16) 57 | 58 | conf['register'] = reg 59 | 60 | return config 61 | 62 | 63 | class I2CToggle(Toggle): 64 | """ I2C Toggle 65 | Turns an I2C relay off and on 66 | """ 67 | 68 | @property 69 | def address(self): 70 | """ I2C address """ 71 | return self.config.get('address', DEVICE_ADDR) 72 | 73 | 74 | @property 75 | def register(self): 76 | """ Register to write to """ 77 | return self.config.get('register', 0x01) 78 | 79 | 80 | """ Methods """ 81 | def init(self): 82 | """ Connect to the relay over I2C """ 83 | super().init() 84 | 85 | if self.invert_state: 86 | self.pin_state_on = 0x00 87 | self.pin_state_off = 0xFF 88 | else: 89 | self.pin_state_on = 0xFF 90 | self.pin_state_off = 0x00 91 | 92 | # Prepare the relay i2c connection 93 | self.bus = smbus2.SMBus(DEVICE_BUS) 94 | 95 | self.bus.write_byte_data(self.address, self.register, self.pin_state_off) 96 | # Active is used to keep track of durations 97 | self.active = False 98 | time.sleep(0.1) 99 | 100 | return True 101 | 102 | 103 | def restore_state(self, state): 104 | """ This is called on start to 105 | restore previous state """ 106 | self.active = True if state.state else False 107 | self.reset_duration() 108 | return 109 | 110 | 111 | """ Actions """ 112 | def toggle(self, data={}): 113 | # Toggle the state 114 | if self.mudpi.is_prepared: 115 | self.active = not self.active 116 | self.bus.write_byte_data(self.address, self.register, self.pin_state_off if self.active else self.pin_state_on) 117 | self.store_state() 118 | 119 | def turn_on(self, data={}): 120 | # Turn on if its not on 121 | if self.mudpi.is_prepared: 122 | if not self.active: 123 | self.bus.write_byte_data(self.address, self.register, self.pin_state_on) 124 | self.active = True 125 | self.store_state() 126 | 127 | def turn_off(self, data={}): 128 | # Turn off if its not off 129 | if self.mudpi.is_prepared: 130 | if self.active: 131 | self.bus.write_byte_data(self.address, self.register, self.pin_state_off) 132 | self.active = False 133 | self.store_state() -------------------------------------------------------------------------------- /mudpi/workers/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | import redis 3 | import threading 4 | from uuid import uuid4 5 | 6 | from mudpi import constants 7 | from mudpi.logger.Logger import Logger, LOG_LEVEL 8 | 9 | 10 | class Worker: 11 | """ Base Worker Class 12 | 13 | A worker is responsible for managing components, 14 | updating component state, configurations ,etc. 15 | 16 | A worker runs on a thread with an interruptable sleep 17 | interaval between update cycles. 18 | """ 19 | def __init__(self, mudpi, config): 20 | self.mudpi = mudpi 21 | self.config = config 22 | self.components = {} 23 | 24 | if self.key is None: 25 | self.config['key'] = f'{self.__class__.__name__}-{uuid4()}' 26 | 27 | self._worker_available = threading.Event() 28 | self._thread = None 29 | self.init() 30 | self.reset_duration() 31 | self.mudpi.workers.register(self.key, self) 32 | 33 | """ Properties """ 34 | @property 35 | def key(self): 36 | """ Return a unique slug id """ 37 | return self.config.get('key').lower() 38 | 39 | @property 40 | def name(self): 41 | """ A friendly display name """ 42 | return self.config.get('name') if self.config.get('name') else self.key.replace("_", " ").title() 43 | 44 | @property 45 | def update_interval(self): 46 | """ Time in seconds between each work cycle update """ 47 | return self.config.get('update_interval', constants.DEFAULT_UPDATE_INTERVAL) 48 | 49 | @property 50 | def is_available(self): 51 | """ Return if worker is available for work """ 52 | return self._worker_available.is_set() 53 | 54 | @is_available.setter 55 | def is_available(self, value): 56 | if bool(value): 57 | self._worker_available.set() 58 | else: 59 | self._worker_available.clear() 60 | 61 | """ Methods """ 62 | def init(self): 63 | """ Called at end of __init__ for additonal setup """ 64 | pass 65 | 66 | def run(self, func=None): 67 | """ Create a thread and return it """ 68 | if not self._thread: 69 | self._thread = threading.Thread(target=self.work, args=(func,)) 70 | Logger.log_formatted(LOG_LEVEL["debug"], 71 | f"Worker {self.key} ", "Starting", "notice") 72 | self._thread.start() 73 | Logger.log_formatted(LOG_LEVEL["info"], 74 | f"Worker {self.key} ", "Started", "success") 75 | return self._thread 76 | 77 | def work(self, func=None): 78 | """ Perform work each cycle like checking devices, 79 | polling sensors, or listening to events. 80 | Worker should sleep based on `update_interval` 81 | """ 82 | while self.mudpi.is_prepared: 83 | if self.mudpi.is_running: 84 | if callable(func): 85 | func() 86 | for key, component in self.components.items(): 87 | if component.should_update: 88 | component.update() 89 | component.store_state() 90 | self.reset_duration() 91 | self._wait(self.update_interval) 92 | # # MudPi Shutting Down, Perform Cleanup Below 93 | Logger.log_formatted(LOG_LEVEL["debug"], 94 | f"Worker {self.key} ", "Stopping", "notice") 95 | for key, component in self.components.items(): 96 | component.unload() 97 | Logger.log_formatted(LOG_LEVEL["info"], 98 | f"Worker {self.key} ", "Offline", "error") 99 | 100 | def _wait(self, duration=0): 101 | """ Sleeps for a given duration 102 | This allows the worker to be interupted 103 | while waiting. 104 | """ 105 | time_remaining = duration 106 | while time_remaining > 0 and self.mudpi.is_prepared and self.duration < duration: 107 | time.sleep(0.001) 108 | time_remaining -= 0.001 109 | 110 | """ Should be moved to Timer util """ 111 | @property 112 | def duration(self): 113 | self.time_elapsed = time.perf_counter() - self.time_start 114 | return round(self.time_elapsed, 4) 115 | 116 | def reset_duration(self): 117 | self.time_start = time.perf_counter() 118 | pass 119 | -------------------------------------------------------------------------------- /mudpi/extensions/sun/trigger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sun Trigger Interface 3 | Checks time against sun data 4 | to help perform actions based on 5 | the suns position. 6 | """ 7 | import json 8 | import time 9 | import datetime 10 | from mudpi.utils import decode_event_data 11 | from mudpi.exceptions import ConfigError 12 | from mudpi.extensions import BaseInterface 13 | from mudpi.extensions.trigger import Trigger 14 | from mudpi.logger.Logger import Logger, LOG_LEVEL 15 | 16 | 17 | class Interface(BaseInterface): 18 | 19 | update_interval = 1 20 | 21 | def load(self, config): 22 | """ Load Trigger component from configs """ 23 | trigger = SunTrigger(self.mudpi, config) 24 | if trigger: 25 | self.add_component(trigger) 26 | return True 27 | 28 | def validate(self, config): 29 | """ Validate the trigger config """ 30 | if not isinstance(config, list): 31 | config = [config] 32 | 33 | for conf in config: 34 | if not conf.get('source'): 35 | raise ConfigError('Missing `source` key in Sun Trigger config.') 36 | 37 | return config 38 | 39 | 40 | class SunTrigger(Trigger): 41 | """ A trigger that checks time 42 | against sun data to perform 43 | actions based on sun position. 44 | """ 45 | 46 | """ Properties """ 47 | @property 48 | def actions(self): 49 | """ Keys of actions to call if triggered """ 50 | return self.config.get('actions', []) 51 | 52 | @property 53 | def nested_source(self): 54 | """ Override the nested_source of sun trigger """ 55 | _types = ['sunset', 'sunrise', 'solar_noon'] 56 | _type = self.config.get('nested_source', None) 57 | return _type if _type in _types else None 58 | 59 | @property 60 | def offset(self): 61 | """ Return a timedelta offset for comparison """ 62 | _offset = self.config.get('offset', {}) 63 | return datetime.timedelta(hours=_offset.get('hours',0), minutes=_offset.get('minutes',0), seconds=_offset.get('seconds',0)) 64 | 65 | 66 | """ Methods """ 67 | def init(self): 68 | """ Listen to the sensors state for changes """ 69 | super().init() 70 | return True 71 | 72 | def update(self): 73 | """ Main update loop to see if trigger should fire """ 74 | self.check_time() 75 | 76 | def check_time(self): 77 | """ Checks the time to see if it is currently sunrise or sunset """ 78 | 79 | # Get state object from manager 80 | state = self.mudpi.states.get(self.source) 81 | 82 | if state is not None: 83 | _state = state.state 84 | else: 85 | _state = None 86 | 87 | if _state: 88 | try: 89 | _value = self._parse_data(_state) 90 | _now = datetime.datetime.now().replace(microsecond=0) 91 | if _value: 92 | _value = datetime.datetime.strptime(_value, "%Y-%m-%d %I:%M:%S %p").replace(microsecond=0) + self.offset 93 | if _now == _value: 94 | self.active = True 95 | if self._previous_state != self.active: 96 | # Trigger is reset, Fire 97 | self.trigger(_value.strftime('%Y-%m-%d %I:%M:%S %p')) 98 | else: 99 | # Trigger not reset check if its multi fire 100 | if self.frequency == 'many': 101 | self.trigger(_value.strftime('%Y-%m-%d %I:%M:%S %p')) 102 | else: 103 | self.active = False 104 | except Exception as error: 105 | Logger.log(LOG_LEVEL["error"], 106 | f'Error evaluating thresholds for trigger {self.id}') 107 | Logger.log(LOG_LEVEL["debug"], error) 108 | self._previous_state = self.active 109 | 110 | def unload(self): 111 | # Unsubscribe once bus supports single handler unsubscribes 112 | return 113 | 114 | def _parse_data(self, data): 115 | """ Get nested data if set otherwise return the data """ 116 | try: 117 | data = json.loads(data) 118 | except Exception as error: 119 | pass 120 | if isinstance(data, dict): 121 | return data if not self.nested_source else data.get(self.nested_source, None) 122 | return data -------------------------------------------------------------------------------- /mudpi/tools/lcd_reset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # The wiring for the LCD is as follows: 4 | # 1 : GND 5 | # 2 : 5V 6 | # 3 : Contrast (0-5V)* 7 | # 4 : RS (Register Select) 8 | # 5 : R/W (Read Write) - GROUND THIS PIN 9 | # 6 : Enable or Strobe 10 | # 7 : Data Bit 0 - NOT USED 11 | # 8 : Data Bit 1 - NOT USED 12 | # 9 : Data Bit 2 - NOT USED 13 | # 10: Data Bit 3 - NOT USED 14 | # 11: Data Bit 4 15 | # 12: Data Bit 5 16 | # 13: Data Bit 6 17 | # 14: Data Bit 7 18 | # 15: LCD Backlight +5V** 19 | # 16: LCD Backlight GND 20 | 21 | # import 22 | import RPi.GPIO as GPIO 23 | import time 24 | 25 | # Define GPIO to LCD mapping 26 | LCD_RS = 7 27 | LCD_E = 8 28 | LCD_D4 = 25 29 | LCD_D5 = 24 30 | LCD_D6 = 23 31 | LCD_D7 = 18 32 | 33 | # Define some device constants 34 | LCD_WIDTH = 16 # Maximum characters per line 35 | LCD_CHR = True 36 | LCD_CMD = False 37 | 38 | LCD_LINE_1 = 0x80 # LCD RAM address for the 1st line 39 | LCD_LINE_2 = 0xC0 # LCD RAM address for the 2nd line 40 | 41 | # Timing constants 42 | E_PULSE = 0.0005 43 | E_DELAY = 0.0005 44 | 45 | 46 | def main(): 47 | prepare_gpio() 48 | 49 | print('Clearing Lcd...') 50 | # Send some test 51 | lcd_string('LCD SCREEN FONT_RESET', LCD_LINE_1) 52 | lcd_string('3 SECONDS LEFT', LCD_LINE_2) 53 | 54 | time.sleep(3) # 3 second delay 55 | 56 | lcd_byte(0x01, LCD_CMD) 57 | lcd_string("FONT_RESET IN PROGRES", LCD_LINE_1) 58 | lcd_string("Shutting Down: 1s", LCD_LINE_2) 59 | time.sleep(1) 60 | lcd_string(" ", LCD_LINE_1) 61 | lcd_string(" ", LCD_LINE_2) 62 | time.sleep(E_DELAY) 63 | lcd_byte(0x01, LCD_CMD) 64 | time.sleep(E_DELAY) 65 | lcd_byte(0x01, LCD_CMD) 66 | time.sleep(E_DELAY) 67 | GPIO.cleanup() 68 | 69 | 70 | def prepare_gpio(): 71 | # Main program block 72 | GPIO.setwarnings(False) 73 | GPIO.setmode(GPIO.BCM) # Use BCM GPIO numbers 74 | GPIO.setup(LCD_E, GPIO.OUT) # E 75 | GPIO.setup(LCD_RS, GPIO.OUT) # RS 76 | GPIO.setup(LCD_D4, GPIO.OUT) # DB4 77 | GPIO.setup(LCD_D5, GPIO.OUT) # DB5 78 | GPIO.setup(LCD_D6, GPIO.OUT) # DB6 79 | GPIO.setup(LCD_D7, GPIO.OUT) # DB7 80 | # Initialise display 81 | lcd_init() 82 | 83 | 84 | def lcd_init(): 85 | # Initialise display 86 | lcd_byte(0x33, LCD_CMD) # 110011 Initialise 87 | lcd_byte(0x32, LCD_CMD) # 110010 Initialise 88 | lcd_byte(0x06, LCD_CMD) # 000110 Cursor move direction 89 | lcd_byte(0x0C, LCD_CMD) # 001100 Display On,Cursor Off, Blink Off 90 | lcd_byte(0x28, LCD_CMD) # 101000 Data length, number of lines, font size 91 | lcd_byte(0x01, LCD_CMD) # 000001 Clear display 92 | time.sleep(E_DELAY) 93 | 94 | 95 | def lcd_byte(bits, mode): 96 | # Send byte to data pins 97 | # bits = data 98 | # mode = True for character 99 | # False for command 100 | 101 | GPIO.output(LCD_RS, mode) # RS 102 | 103 | # High bits 104 | GPIO.output(LCD_D4, False) 105 | GPIO.output(LCD_D5, False) 106 | GPIO.output(LCD_D6, False) 107 | GPIO.output(LCD_D7, False) 108 | if bits & 0x10 == 0x10: 109 | GPIO.output(LCD_D4, True) 110 | if bits & 0x20 == 0x20: 111 | GPIO.output(LCD_D5, True) 112 | if bits & 0x40 == 0x40: 113 | GPIO.output(LCD_D6, True) 114 | if bits & 0x80 == 0x80: 115 | GPIO.output(LCD_D7, True) 116 | 117 | # Toggle 'Enable' pin 118 | lcd_toggle_enable() 119 | 120 | # Low bits 121 | GPIO.output(LCD_D4, False) 122 | GPIO.output(LCD_D5, False) 123 | GPIO.output(LCD_D6, False) 124 | GPIO.output(LCD_D7, False) 125 | if bits & 0x01 == 0x01: 126 | GPIO.output(LCD_D4, True) 127 | if bits & 0x02 == 0x02: 128 | GPIO.output(LCD_D5, True) 129 | if bits & 0x04 == 0x04: 130 | GPIO.output(LCD_D6, True) 131 | if bits & 0x08 == 0x08: 132 | GPIO.output(LCD_D7, True) 133 | 134 | # Toggle 'Enable' pin 135 | lcd_toggle_enable() 136 | 137 | 138 | def lcd_toggle_enable(): 139 | # Toggle enable 140 | time.sleep(E_DELAY) 141 | GPIO.output(LCD_E, True) 142 | time.sleep(E_PULSE) 143 | GPIO.output(LCD_E, False) 144 | time.sleep(E_DELAY) 145 | 146 | 147 | def lcd_string(message, line): 148 | # Send string to display 149 | 150 | message = message.ljust(LCD_WIDTH, " ") 151 | 152 | lcd_byte(line, LCD_CMD) 153 | 154 | for i in range(LCD_WIDTH): 155 | lcd_byte(ord(message[i]), LCD_CHR) 156 | 157 | 158 | if __name__ == '__main__': 159 | main() 160 | -------------------------------------------------------------------------------- /mudpi/debug/dump.py: -------------------------------------------------------------------------------- 1 | """Dump all possible values from the AVR.""" 2 | 3 | import inspect 4 | from nanpy.arduinotree import ArduinoTree 5 | from nanpy.serialmanager import SerialManager 6 | from pprint import pprint 7 | from nanpy.classinfo import FirmwareMissingFeatureError 8 | 9 | FORMAT = '%-20s = %20s' 10 | 11 | 12 | def dump(obj, selected_names=None): 13 | if selected_names: 14 | ls = selected_names 15 | else: 16 | ls = dir(obj) 17 | for attr in ls: 18 | if not attr.startswith('__'): 19 | if not inspect.ismethod(getattr(obj, attr)): 20 | print(FORMAT % (attr, getattr(obj, attr))) 21 | 22 | 23 | def dump_dict(d): 24 | for defname in sorted(d.keys()): 25 | defvalue = d[defname] 26 | print(FORMAT % (defname, defvalue)) 27 | 28 | def myprint(template, name, func): 29 | try: 30 | print(template % (name, func())) 31 | except FirmwareMissingFeatureError: 32 | pass 33 | 34 | def dumpall(): 35 | connection = SerialManager() 36 | a = ArduinoTree(connection=connection) 37 | 38 | if a.vcc: 39 | myprint(FORMAT + ' V', 'read_vcc', lambda : a.vcc.read()) 40 | myprint(FORMAT + ' sec', 'uptime', lambda : a.api.millis() / 1000.0) 41 | 42 | print('') 43 | print('================================') 44 | print('firmware classes:') 45 | print('================================') 46 | pprint(a.connection.classinfo.firmware_name_list) 47 | 48 | print('') 49 | print('================================') 50 | print('defines:') 51 | print('================================') 52 | dump_dict(a.define.as_dict) 53 | 54 | if a.esp: 55 | print('') 56 | print('================================') 57 | print('ESP:') 58 | print('================================') 59 | myprint(FORMAT , 'getVcc', lambda :a.esp.getVcc()) 60 | myprint(FORMAT , 'getFreeHeap', lambda :a.esp.getFreeHeap()) 61 | myprint(FORMAT , 'getChipId', lambda :a.esp.getChipId()) 62 | myprint(FORMAT , 'getSdkVersion', lambda :a.esp.getSdkVersion()) 63 | myprint(FORMAT , 'getBootVersion', lambda :a.esp.getBootVersion()) 64 | myprint(FORMAT , 'getBootMode', lambda :a.esp.getBootMode()) 65 | myprint(FORMAT , 'getCpuFreqMHz', lambda :a.esp.getCpuFreqMHz()) 66 | myprint(FORMAT , 'getFlashChipId', lambda :a.esp.getFlashChipId()) 67 | myprint(FORMAT , 'getFlashChipRealSize', lambda :a.esp.getFlashChipRealSize()) 68 | myprint(FORMAT , 'getFlashChipSize', lambda :a.esp.getFlashChipSize()) 69 | myprint(FORMAT , 'getFlashChipSpeed', lambda :a.esp.getFlashChipSpeed()) 70 | myprint(FORMAT , 'getFlashChipMode', lambda :a.esp.getFlashChipMode()) 71 | myprint(FORMAT , 'getFlashChipSizeByChipId', lambda :a.esp.getFlashChipSizeByChipId()) 72 | myprint(FORMAT , 'getResetReason', lambda :a.esp.getResetReason()) 73 | myprint(FORMAT , 'getResetInfo', lambda :a.esp.getResetInfo()) 74 | myprint(FORMAT , 'getSketchSize', lambda :a.esp.getSketchSize()) 75 | myprint(FORMAT , 'getFreeSketchSpace', lambda :a.esp.getFreeSketchSpace()) 76 | 77 | print('') 78 | print('================================') 79 | print('pins:') 80 | print('================================') 81 | 82 | myprint(FORMAT , 'total_pin_count', lambda :a.pin.count) 83 | myprint(FORMAT , 'digital_names', lambda :a.pin.names_digital) 84 | myprint(FORMAT , 'analog_names', lambda :a.pin.names_analog) 85 | 86 | for pin_number in range(a.pin.count): 87 | print('---------- pin_number=%s ---------------' % pin_number) 88 | pin = a.pin.get(pin_number) 89 | dump( 90 | pin, 91 | 'name pin_number pin_number_analog is_digital is_analog avr_pin mode digital_value analog_value programming_function'.split()) 92 | if pin.pwm.available: 93 | print('--- pwm ---') 94 | dump(pin.pwm, '''frequency frequencies_available base_divisor divisor divisors_available 95 | timer_mode 96 | timer_register_name_a 97 | timer_register_name_b 98 | wgm 99 | '''.split()) 100 | 101 | 102 | if a.register: 103 | print('') 104 | print('================================') 105 | print('registers:') 106 | print('================================') 107 | for x in a.register.names: 108 | r = a.register.get(x) 109 | if r.size == 2: 110 | v = '0x%04X' % r.value 111 | else: 112 | v = ' 0x%02X' % r.value 113 | 114 | print('%-20s = %s @0x%2X (size:%s)' % (r.name, v, r.address, r.size)) 115 | 116 | 117 | 118 | if __name__ == '__main__': 119 | dumpall() -------------------------------------------------------------------------------- /mudpi/server/mudpi_event_listener.js: -------------------------------------------------------------------------------- 1 | var Redis = require('ioredis'); 2 | var listener = new Redis({ showFriendlyErrorStack: true }); 3 | var plistener = new Redis({ showFriendlyErrorStack: true }); //listener.createClient(); 4 | const axios = require('axios') 5 | 6 | // Mudpi Event Relay Server v0.2 7 | // This is a nodejs script to catch redis events emitted by the mudpi core and 8 | // relay them to a webhook for further actions such as logging to a database. 9 | 10 | 11 | // CONFIGS ------------------------- 12 | const address = 'test.php' 13 | const channel = '*'; 14 | let axiosConfig = { 15 | headers: { 16 | 'Content-Type': 'application/json;charset=UTF-8', 17 | "Access-Control-Allow-Origin": "*", 18 | }, 19 | baseURL: 'http://192.168.2.230/', 20 | timeout: 1000, 21 | responseType: 'json' 22 | }; 23 | //------------------------------ 24 | 25 | function sleep(ms) { 26 | return new Promise(resolve => setTimeout(resolve, ms)); 27 | } 28 | 29 | 30 | 31 | plistener.on('connect', function() { 32 | console.log('\x1b[32mRedis Pattern Listener connected\x1b[0m'); 33 | }); 34 | 35 | plistener.on('error', function (err) { 36 | console.log('PListener Something went wrong ' + err); 37 | }); 38 | 39 | listener.on('connect', function() { 40 | console.log('\x1b[32mRedis Listener connected\x1b[0m'); 41 | }); 42 | 43 | listener.on('error', function (err) { 44 | console.log('Listener Something went wrong ' + err); 45 | }); 46 | 47 | 48 | let now = new Date().toString().replace(/T/, ':').replace(/\.\w*/, ''); 49 | plistener.set('mudpi_relay_started_at', now); 50 | plistener.get('started_at', function (error, result) { 51 | if (error) { 52 | console.log(error); 53 | throw error; 54 | } 55 | 56 | console.log('Event Relay Started at -> \x1b[32m%s\x1b[0m', now); 57 | console.log('MudPi System Started at -> \x1b[32m%s\x1b[0m', result); 58 | }); 59 | 60 | 61 | listener.subscribe(channel, (error, count) => { 62 | if (error) { 63 | throw new Error(error); 64 | } 65 | console.log(`Subscribed to \x1b[36m${count} channel.\x1b[0m Listening for updates on the \x1b[36m${channel} channel.\x1b[0m`); 66 | }); 67 | 68 | listener.on('message', (channel, message) => { 69 | console.log(`\x1b[36mMessage Received -\x1b[0m \x1b[37m${channel}\x1b[0m`); 70 | }); 71 | 72 | 73 | plistener.psubscribe(channel, (error, count) => { 74 | if (error) { 75 | throw new Error(error); 76 | } 77 | console.log(`Client Subscribed to \x1b[36m${count} channel.\x1b[0m Listening for updates on the \x1b[36m${channel} channel.\x1b[0m`); 78 | }); 79 | 80 | plistener.on('pmessage', (pattern, channel, message) => { 81 | console.log(`\x1b[36mPattern Message Received on \x1b[1m${channel}\x1b[0m`); 82 | let eventPromise = relayEvent(message) 83 | eventPromise.then((response) => { 84 | try { 85 | console.log(`\x1b[32mEvent Successfully Relayed. RESPONSE: %s\x1b[0m`, response.status) 86 | 87 | if(typeof response !== 'undefined' ) { 88 | if (response != null && response.hasOwnProperty('data')) { 89 | console.log('\x1b[32mResponse Data Received: \x1b[0m', response.data) 90 | } 91 | } 92 | } 93 | catch(error) { 94 | console.log('\x1b[32mEvent Successfully Relayed.\x1b[0m \x1b[31mRESPONSE: Error Decoding Response\x1b[0m') 95 | } 96 | }) 97 | .catch((error) => { 98 | console.log('\x1b[31mRelaying Event FAILED:\x1b[0m') 99 | console.log(error) 100 | }) 101 | //console.log(data) 102 | console.log('\x1b[33mAttempting to Relay Event: \x1b[1m', message, '\x1b[0m') 103 | }); 104 | 105 | 106 | async function relayEvent(event=null) { 107 | let relayedEvent = null 108 | 109 | try { 110 | relayedEvent = await axios.post(address, event, axiosConfig) 111 | } 112 | catch(e) { 113 | if(e.code == 'ENETUNREACH') { 114 | let retries = 3 115 | while(retries > 1 ) { 116 | 117 | try { 118 | console.log('\x1b[31mRelaying the Event Failed [', e.code, ']\x1b[0m') 119 | await sleep(5000) 120 | console.log('\x1b[33mRetrying Relaying the Event...\x1b[0m') 121 | relayedEvent = await axios.post(address, event, axiosConfig) 122 | } 123 | catch(e) { 124 | console.log('\x1b[31mProblem Resending the Event [', e.code, ']\x1b[0m') 125 | } 126 | 127 | retries-- 128 | } 129 | } 130 | else { 131 | console.log('\x1b[31mProblem Relaying the Event:\x1b[0m') 132 | console.error(e) 133 | } 134 | } 135 | 136 | return relayedEvent 137 | } 138 | 139 | -------------------------------------------------------------------------------- /mudpi/extensions/dht_legacy/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | DHT Sensor Interface 3 | Connects to a DHT device to get 4 | humidity and temperature readings. 5 | """ 6 | import time 7 | 8 | import Adafruit_DHT 9 | 10 | from mudpi.constants import METRIC_SYSTEM 11 | from mudpi.extensions import BaseInterface 12 | from mudpi.extensions.sensor import Sensor 13 | from mudpi.logger.Logger import Logger, LOG_LEVEL 14 | from mudpi.exceptions import MudPiError, ConfigError 15 | 16 | 17 | class Interface(BaseInterface): 18 | 19 | def load(self, config): 20 | """ Load DHT sensor component from configs """ 21 | sensor = DHTSensor(self.mudpi, config) 22 | if sensor: 23 | self.add_component(sensor) 24 | return True 25 | 26 | def validate(self, config): 27 | """ Validate the dht config """ 28 | if not isinstance(config, list): 29 | config = [config] 30 | 31 | for conf in config: 32 | if not conf.get('pin'): 33 | raise ConfigError('Missing `pin` in DHT config.') 34 | 35 | if str(conf.get('model')) not in DHTSensor.models: 36 | conf['model'] = '11' 37 | Logger.log( 38 | LOG_LEVEL["warning"], 39 | 'Sensor Model Error: Defaulting to DHT11' 40 | ) 41 | 42 | return config 43 | 44 | 45 | class DHTSensor(Sensor): 46 | """ DHT Sensor 47 | Returns a random number 48 | """ 49 | 50 | # Number of attempts to get a good reading (careful to not lock worker!) 51 | _read_attempts = 3 52 | 53 | # Models of dht devices 54 | models = { 55 | '11': Adafruit_DHT.DHT11, 56 | '22': Adafruit_DHT.DHT22, 57 | '2302': Adafruit_DHT.AM2302 58 | } # AM2302 = DHT22 59 | 60 | """ Properties """ 61 | 62 | @property 63 | def id(self): 64 | """ Return a unique id for the component """ 65 | return self.config['key'] 66 | 67 | @property 68 | def name(self): 69 | """ Return the display name of the component """ 70 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 71 | 72 | @property 73 | def state(self): 74 | """ Return the state of the component (from memory, no IO!) """ 75 | return self._state 76 | 77 | @property 78 | def classifier(self): 79 | """ Classification further describing it, effects the data formatting """ 80 | return 'climate' 81 | 82 | @property 83 | def type(self): 84 | """ Model of the device """ 85 | return str(self.config.get('model', '11')) 86 | 87 | @property 88 | def pin(self): 89 | """ Return a pin for the component """ 90 | return self.config['pin'] 91 | 92 | 93 | """ Methods """ 94 | def init(self): 95 | """ Connect to the device """ 96 | self._sensor = None 97 | 98 | if self.type in self.models: 99 | self._dht_device = self.models[self.type] 100 | 101 | self.check_dht() 102 | 103 | return True 104 | 105 | def check_dht(self): 106 | """ Check if the DHT device is setup """ 107 | if self._sensor is None: 108 | try: 109 | self._sensor = self._dht_device 110 | except Exception as error: 111 | Logger.log( 112 | LOG_LEVEL["error"], 113 | 'Sensor Initialize Error: DHT (Legacy) Failed to Init' 114 | ) 115 | self._sensor = None 116 | Logger.log( 117 | LOG_LEVEL["debug"], 118 | error 119 | ) 120 | return False 121 | return True 122 | 123 | def update(self): 124 | """ Get data from DHT device""" 125 | humidity = None 126 | temperature_c = None 127 | 128 | if self.check_dht(): 129 | try: 130 | humidity, temperature_c = Adafruit_DHT.read_retry(self._sensor, self.pin) 131 | except Exception as error: 132 | # Errors happen fairly often, DHT's are hard to read 133 | Logger.log(LOG_LEVEL["debug"], error) 134 | 135 | if humidity is not None and temperature_c is not None: 136 | _temperature = temperature_c if self.mudpi.unit_system == METRIC_SYSTEM else (temperature_c * 1.8 + 32) 137 | readings = { 138 | 'temperature': round(_temperature, 2), 139 | 'humidity': round(humidity, 2) 140 | } 141 | self._state = readings 142 | return readings 143 | else: 144 | Logger.log( 145 | LOG_LEVEL["debug"], 146 | f'DHT Reading was Invalid (Legacy).' 147 | ) 148 | time.sleep(2.1) 149 | return None 150 | -------------------------------------------------------------------------------- /mudpi/extensions/mqtt/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | MQTT Sensor Interface 3 | Connects to a mqtt to get data 4 | from an incoming event. 5 | """ 6 | import time 7 | import json 8 | from mudpi.utils import decode_event_data 9 | from mudpi.extensions import BaseInterface 10 | from mudpi.extensions.sensor import Sensor 11 | from mudpi.logger.Logger import Logger, LOG_LEVEL 12 | from mudpi.exceptions import MudPiError, ConfigError 13 | 14 | 15 | class Interface(BaseInterface): 16 | 17 | # Override the update interval due to event handling 18 | update_interval = 1 19 | 20 | # Duration tracking 21 | _duration_start = time.perf_counter() 22 | 23 | def load(self, config): 24 | """ Load mqtt sensor component from configs """ 25 | sensor = MQTTSensor(self.mudpi, config) 26 | if sensor: 27 | sensor.connect(self.extension) 28 | self.add_component(sensor) 29 | return True 30 | 31 | def validate(self, config): 32 | """ Validate the mqtt sensor config """ 33 | if not isinstance(config, list): 34 | config = [config] 35 | 36 | for conf in config: 37 | if not conf.get('key'): 38 | raise ConfigError('Missing `key` in MQTT sensor config.') 39 | 40 | expires = conf.get('expires') 41 | if not expires: 42 | conf['expires'] = 0 43 | else: 44 | conf['expires'] = int(conf['expires']) 45 | 46 | return config 47 | 48 | 49 | class MQTTSensor(Sensor): 50 | """ MQTT Sensor 51 | Returns a reading from events 52 | """ 53 | 54 | """ Properties """ 55 | @property 56 | def id(self): 57 | """ Return a unique id for the component """ 58 | return self.config['key'] 59 | 60 | @property 61 | def name(self): 62 | """ Return the display name of the component """ 63 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 64 | 65 | @property 66 | def state(self): 67 | """ Return the state of the component (from memory, no IO!) """ 68 | return self._state 69 | 70 | @property 71 | def classifier(self): 72 | """ Classification further describing it, effects the data formatting """ 73 | return self.config.get('classifier', 'general') 74 | 75 | @property 76 | def topic(self): 77 | """ Return the topic to listen on for event sensors """ 78 | return str(self.config.get('topic', f'sensor/{self.id}')) 79 | 80 | @property 81 | def expires(self): 82 | """ Return the time in which state becomes stale """ 83 | return int(self.config.get('expires', 0)) 84 | 85 | @property 86 | def expired(self): 87 | """ Return if current data is expired """ 88 | if self.expires > 0: 89 | return time.perf_counter() - self._duration_start > self.expires 90 | else: 91 | return False 92 | 93 | 94 | """ Methods """ 95 | def init(self): 96 | """ Connect to the device """ 97 | # Perform inital state fetch 98 | # self.update() 99 | # self.store_state() 100 | 101 | # Track state change 102 | self._prev_state = None 103 | 104 | # Connection to mqtt 105 | self._conn = None 106 | 107 | # For duration tracking 108 | self._duration_start = time.perf_counter() 109 | 110 | return True 111 | 112 | def connect(self, extension): 113 | """ Connect the sensor to mqtt """ 114 | _conn_key = self.config['connection'] 115 | self._conn = extension.connections[_conn_key]['client'] 116 | extension.subscribe(_conn_key, self.topic, self.handle_event) 117 | 118 | 119 | def update(self): 120 | """ Get data from memory or wait for event """ 121 | if self._conn: 122 | if self.expired: 123 | self.mudpi.events.publish('sensor', { 124 | 'event': 'StateExpired', 125 | 'component_id': self.id, 126 | 'expires': self.expires, 127 | 'previous_state': self.state, 128 | 'type': self.type}) 129 | self._state = None 130 | if self._prev_state != self._state: 131 | self.reset_duration() 132 | self._prev_state = self._state 133 | return self._state 134 | 135 | def handle_event(self, data={}): 136 | """ Handle event from mqtt broker """ 137 | if data is not None: 138 | try: 139 | # _event_data = self.last_event = decode_event_data(data) 140 | self._state = data 141 | except: 142 | Logger.log( 143 | LOG_LEVEL["info"], 144 | f"Error Decoding Event for MQTT Sensor {self.id}" 145 | ) 146 | 147 | def reset_duration(self): 148 | """ Reset the duration of the current state """ 149 | self._duration_start = time.perf_counter() 150 | return True -------------------------------------------------------------------------------- /mudpi/workers/adc_worker.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import busio 4 | import board 5 | import redis 6 | import digitalio 7 | import threading 8 | import importlib 9 | import adafruit_mcp3xxx.mcp3008 as MCP 10 | from adafruit_mcp3xxx.analog_in import AnalogIn 11 | 12 | from mudpi.logger.Logger import Logger, LOG_LEVEL 13 | 14 | 15 | class ADCMCP3008Worker: 16 | """ 17 | Analog-Digital-Converter Worker 18 | """ 19 | PINS = { 20 | '4': board.D4, 21 | '17': board.D17, 22 | '27': board.D27, 23 | '22': board.D22, 24 | '5': board.D5, 25 | '6': board.D6, 26 | '13': board.D13, 27 | '19': board.D19, 28 | '26': board.D26, 29 | '18': board.D18, 30 | '23': board.D23, 31 | '24': board.D24, 32 | '25': board.D25, 33 | '12': board.D12, 34 | '16': board.D16, 35 | '20': board.D20, 36 | '21': board.D21 37 | } 38 | 39 | def __init__(self, config: dict, main_thread_running, system_ready): 40 | self.config = config 41 | self.main_thread_running = main_thread_running 42 | self.system_ready = system_ready 43 | self.node_ready = False 44 | try: 45 | self.r = redis_conn if redis_conn is not None else redis.Redis( 46 | host='127.0.0.1', port=6379) 47 | except KeyError: 48 | self.r = redis.Redis(host='127.0.0.1', port=6379) 49 | 50 | spi = busio.SPI(clock=board.SCK, MISO=board.MISO, MOSI=board.MOSI) 51 | cs = digitalio.DigitalInOut(ADCMCP3008Worker.PINS[config['pin']]) 52 | 53 | self.mcp = MCP.MCP3008(spi, cs) 54 | 55 | self.sensors = [] 56 | self.init_sensors() 57 | 58 | self.node_ready = True 59 | 60 | def dynamic_sensor_import(self, path): 61 | components = path.split('.') 62 | 63 | s = '' 64 | for component in components[:-1]: 65 | s += component + '.' 66 | 67 | parent = importlib.import_module(s[:-1]) 68 | sensor = getattr(parent, components[-1]) 69 | 70 | return sensor 71 | 72 | def init_sensors(self): 73 | for sensor in self.config['sensors']: 74 | 75 | if sensor.get('type', None) is not None: 76 | # Get the sensor from the sensors folder 77 | # {sensor name}_sensor.{SensorName}Sensor 78 | sensor_type = 'sensors.mcp3xxx.' + sensor.get( 79 | 'type').lower() + '_sensor.' + sensor.get( 80 | 'type').capitalize() + 'Sensor' 81 | # analog_pin_mode = False if sensor.get('is_digital', False) else True 82 | imported_sensor = self.dynamic_sensor_import(sensor_type) 83 | new_sensor = imported_sensor(int(sensor.get('pin')), 84 | name=sensor.get('name', 85 | sensor.get( 86 | 'type')), 87 | key=sensor.get('key', None), 88 | mcp=self.mcp) 89 | new_sensor.init_sensor() 90 | self.sensors.append(new_sensor) 91 | Logger.log( 92 | LOG_LEVEL["info"], 93 | '{type} Sensor {pin}...\t\t\t\033[1;32m Ready\033[0;0m'.format( 94 | **sensor) 95 | ) 96 | 97 | def run(self): 98 | 99 | if self.node_ready: 100 | t = threading.Thread(target=self.work, args=()) 101 | t.start() 102 | Logger.log( 103 | LOG_LEVEL["info"], 104 | str(self.config['name']) + ' Node Worker [' + str( 105 | len(self.config[ 106 | 'sensors'])) + ' Sensors]...\t\033[1;32m Online\033[0;0m' 107 | ) 108 | return t 109 | 110 | else: 111 | Logger.log( 112 | LOG_LEVEL["warning"], 113 | "Node Connection...\t\t\t\033[1;31m Failed\033[0;0m" 114 | ) 115 | return None 116 | 117 | def work(self): 118 | 119 | while self.main_thread_running.is_set(): 120 | if self.system_ready.is_set() and self.node_ready: 121 | message = {'event': 'SensorUpdate'} 122 | readings = {} 123 | for sensor in self.sensors: 124 | result = sensor.read() 125 | readings[sensor.key] = result 126 | # r.set(sensor.get('key', sensor.get('type')), value) 127 | 128 | Logger.log(LOG_LEVEL["info"], readings) 129 | message['data'] = readings 130 | self.r.publish('sensors', json.dumps(message)) 131 | 132 | time.sleep(15) 133 | # This is only ran after the main thread is shut down 134 | Logger.log( 135 | LOG_LEVEL["info"], 136 | "{name} Node Worker Shutting Down...\t\t\033[1;32m Complete\033[0;0m".format( 137 | **self.config) 138 | ) 139 | -------------------------------------------------------------------------------- /examples/automation_sequence/mudpi.json: -------------------------------------------------------------------------------- 1 | { 2 | "mudpi": { 3 | "name": "MudPi Sequence Example", 4 | "debug": true, 5 | "location": { 6 | "latitude": 40.26, 7 | "longitude": 9.043 8 | } 9 | }, 10 | "trigger": [ 11 | { 12 | "interface": "state", 13 | "source": "example_soil", 14 | "key": "soil_dry", 15 | "name": "Dry Soil", 16 | "frequency": "once", 17 | "actions": [ 18 | ".example_toggle.turn_on" 19 | ], 20 | "thresholds": [ 21 | { 22 | "comparison": "gte", 23 | "value": 450 24 | } 25 | ] 26 | }, 27 | { 28 | "interface": "state", 29 | "source": "example_soil", 30 | "key": "soil_wet_state_example", 31 | "name": "Wet Soil", 32 | "frequency": "once", 33 | "actions": [ 34 | ".example_toggle.turn_off" 35 | ], 36 | "thresholds": [ 37 | { 38 | "comparison": "lte", 39 | "value": 449 40 | } 41 | ] 42 | }, 43 | { 44 | "interface": "sensor", 45 | "source": "example_soil", 46 | "key": "soil_wet_sensor_example", 47 | "name": "Wet Soil", 48 | "frequency": "once", 49 | "actions": [ 50 | ".example_toggle.turn_off" 51 | ], 52 | "thresholds": [ 53 | { 54 | "comparison": "lte", 55 | "value": 449 56 | } 57 | ] 58 | }, 59 | { 60 | "interface": "cron", 61 | "key": "every_2_min", 62 | "name": "Every 2 Mins", 63 | "schedule": "*/2 * * * *", 64 | "actions": [ 65 | ".example_sequence.next_step" 66 | ] 67 | } 68 | ], 69 | "action": [ 70 | { 71 | "type": "event", 72 | "name": "Test Event 1", 73 | "key": "test_1", 74 | "topic": "test", 75 | "action": { 76 | "event": "Test", 77 | "data": "Step 1 Reached" 78 | } 79 | }, 80 | { 81 | "type": "event", 82 | "name": "Test Event 2", 83 | "key": "test_2", 84 | "topic": "test", 85 | "action": { 86 | "event": "Test", 87 | "data": "Step 2 Reached" 88 | } 89 | }, 90 | { 91 | "type": "event", 92 | "name": "Test Event 3", 93 | "key": "test_3", 94 | "topic": "test", 95 | "action": { 96 | "event": "Test", 97 | "data": "Step 3 Reached" 98 | } 99 | }, 100 | { 101 | "type": "event", 102 | "name": "Test Event 4", 103 | "key": "test_4", 104 | "topic": "test", 105 | "action": { 106 | "event": "Test", 107 | "data": "Step 4 Reached" 108 | } 109 | } 110 | ], 111 | "sequence": [ 112 | { 113 | "name": "Example Sequence", 114 | "key": "example_sequence", 115 | "sequence": [ 116 | { 117 | "actions": [ 118 | ".example_pump.turn_on", 119 | ".test_1" 120 | ], 121 | "duration": 3 122 | }, 123 | { 124 | "actions": [ 125 | ".example_pump.turn_off", 126 | ".test_2" 127 | ], 128 | "duration": 3 129 | }, 130 | { 131 | "actions": [ 132 | ".example_pump.turn_on", 133 | ".test_3" 134 | ], 135 | "duration": 10, 136 | "thresholds": [ 137 | { 138 | "source": "example_soil", 139 | "comparison": "gte", 140 | "value": 400 141 | } 142 | ] 143 | }, 144 | { 145 | "actions": [ 146 | ".example_toggle.turn_on", 147 | ".test_4" 148 | ], 149 | "duration": 10 150 | }, 151 | { 152 | "actions": [ 153 | ".example_pump.turn_off" 154 | ], 155 | "duration": 10 156 | } 157 | ] 158 | } 159 | ], 160 | "sensor": [ 161 | { 162 | "interface": "example", 163 | "key": "example_soil", 164 | "data": 350, 165 | "update_interval": 10 166 | } 167 | ], 168 | "toggle": [ 169 | { 170 | "key": "example_pump", 171 | "interface": "example" 172 | }, 173 | { 174 | "key": "example_toggle", 175 | "interface": "example" 176 | } 177 | ] 178 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

MudPi Smart Automation for the Garden & Home

2 | 3 | # MudPi Smart Automation for the Garden & Home 4 | > A python package to gather sensor readings, trigger components, control devices and more in an event based system that can be run on a linux SBC, including Raspberry Pi. 5 | 6 | 7 | ## Documentation 8 | For full documentation visit [mudpi.app](https://mudpi.app/docs) 9 | 10 | 11 | ## Installation 12 | Install MudPi using the [Installer](https://github.com/mudpi/installer) that will guide you through the process. You can also view the [manual install instructions](https://github.com/mudpi/installer/blob/master/docs/MANUAL_INSTALL.md) 13 | 14 | 15 | ## Guides 16 | For examples and guides on how to setup and use MudPi check out the [free guides available.](https://mudpi.app/guides) 17 | 18 | 19 | ## Funding 20 | MudPi core is open source and has a bunch of free resources built around it. Being a solo developer it has become quite a lot to maintain the various areas of the project. It makes me happy seeing how MudPi has helped many people worldwide. I would like to continue working on MudPi and put even more time into the project to take on bigger ambitions. However it has come to a point that MudPi is taking more time to build with little to no income to help justify it. I really could use help from the community to continue building MudPi. If you like my work and MudPi please consider [helping me fund the project and keep the lights on.](https://www.patreon.com/mudpi) 21 | 22 | 23 | ## Contributing 24 | Any contributions you can make will be greatly appreciated. If you are interested in contributing please get in touch with me and submit a pull request. There is much more I would like to add support for, however being a single developer limits my scope. Therefore mainly bugs will be accepted as issues. 25 | 26 | 27 | ## Versioning 28 | Breaking.Major.Minor 29 | 30 | 31 | ## Authors 32 | * Eric Davisson - [Website](http://ericdavisson.com) 33 | * [Twitter.com/theDavisson](https://twitter.com/theDavisson) 34 | 35 | ## Community 36 | * Discord - [Join](https://discord.gg/daWg2YH) 37 | * [Twitter.com/MudpiApp](https://twitter.com/mudpiapp) 38 | 39 | 75 | 76 | ## MudPi Hardware 77 | There are [custom circuit boards designed around MudPi available.](https://mudpi.app/boards) 78 | 79 | ## License 80 | This project is licensed under the BSD-4-Clause License - see the [LICENSE.md](LICENSE.md) file for details 81 | 82 | 83 | MudPi Smart Garden 84 | -------------------------------------------------------------------------------- /mudpi/extensions/timer/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Timer Sensor Interface 3 | Returns a the time elapsed 4 | if the timer is active 5 | """ 6 | import time 7 | from mudpi.extensions import BaseInterface 8 | from mudpi.extensions.sensor import Sensor 9 | from mudpi.logger.Logger import Logger, LOG_LEVEL 10 | from mudpi.constants import FONT_RESET, FONT_MAGENTA 11 | 12 | 13 | class Interface(BaseInterface): 14 | 15 | update_interval = 1 16 | 17 | def load(self, config): 18 | """ Load timer sensor component from configs """ 19 | sensor = TimerSensor(self.mudpi, config) 20 | self.add_component(sensor) 21 | return True 22 | 23 | def register_actions(self): 24 | """ Register any interface actions """ 25 | self.register_component_actions('start', action='start') 26 | self.register_component_actions('stop', action='stop') 27 | self.register_component_actions('reset', action='reset') 28 | self.register_component_actions('pause', action='pause') 29 | self.register_component_actions('restart', action='restart') 30 | 31 | 32 | class TimerSensor(Sensor): 33 | """ Timer Sensor 34 | Return elapsed time 35 | """ 36 | 37 | """ Properties """ 38 | @property 39 | def id(self): 40 | """ Return a unique id for the component """ 41 | return self.config['key'] 42 | 43 | @property 44 | def name(self): 45 | """ Return the display name of the component """ 46 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 47 | 48 | @property 49 | def state(self): 50 | """ Return the state of the component (from memory, no IO!) """ 51 | _state = { 52 | 'active': self.active 53 | } 54 | if self.invert_count: 55 | _remaining = round(self.duration, 2) 56 | _state['duration_remaining'] = _remaining if _remaining > 0 else 0 57 | _state['duration'] = self.duration if self.duration > 0 else 0 58 | else: 59 | _remaining = round(self.max_duration - self.duration, 2) 60 | _state['duration_remaining'] = _remaining if _remaining > 0 else 0 61 | _state['duration'] = self.duration if self.duration < self.max_duration else self.max_duration 62 | return _state 63 | 64 | @property 65 | def max_duration(self): 66 | """ Return the max_duration of the timer """ 67 | return self.config.get('duration', 10) 68 | 69 | @property 70 | def classifier(self): 71 | """ Classification further describing it, effects the data formatting """ 72 | return self.config.get('classifier', "general") 73 | 74 | @property 75 | def active(self): 76 | """ Return if the timer is active or not """ 77 | return self._active 78 | 79 | @property 80 | def duration(self): 81 | if self.active: 82 | self.time_elapsed = (time.perf_counter() - self.time_start) + self._pause_offset 83 | return round(self.time_elapsed, 2) if not self.invert_count else round((self.max_duration - self.time_elapsed), 2) 84 | 85 | @property 86 | def invert_count(self): 87 | """ Return true if count should count down """ 88 | return self.config.get('invert_count', False) 89 | 90 | 91 | """ Methods """ 92 | def init(self): 93 | """ Init the timer component """ 94 | self._active = False 95 | self.time_elapsed = 0 96 | self._pause_offset = 0 97 | self.reset_duration() 98 | 99 | def update(self): 100 | """ Get timer data """ 101 | if self.duration >= self.max_duration and not self.invert_count: 102 | self.stop() 103 | elif self.duration <= 0 and self.invert_count: 104 | self.stop() 105 | return True 106 | 107 | def reset_duration(self): 108 | """ Reset the elapsed duration """ 109 | self.time_start = time.perf_counter() 110 | return self 111 | 112 | 113 | """ Actions """ 114 | def start(self, data=None): 115 | """ Start the timer """ 116 | if not self.active: 117 | self.reset_duration() 118 | self._active = True 119 | if self._pause_offset == 0: 120 | Logger.log( 121 | LOG_LEVEL["debug"], 122 | f'Timer Sensor {FONT_MAGENTA}{self.name}{FONT_RESET} Started' 123 | ) 124 | else: 125 | Logger.log( 126 | LOG_LEVEL["debug"], 127 | f'Timer Sensor {FONT_MAGENTA}{self.name}{FONT_RESET} Resumed' 128 | ) 129 | 130 | def pause(self, data=None): 131 | """ Pause the timer """ 132 | if self.active: 133 | self._active = False 134 | self._pause_offset = 0 135 | self.reset_duration() 136 | 137 | def stop(self, data=None): 138 | """ Stop the timer """ 139 | if self.active: 140 | self._active = False 141 | self.reset() 142 | Logger.log( 143 | LOG_LEVEL["debug"], 144 | f'Timer Sensor {FONT_MAGENTA}{self.name}{FONT_RESET} Stopped' 145 | ) 146 | 147 | def reset(self, data=None): 148 | """ Reset the timer """ 149 | self.reset_duration() 150 | self._pause_offset = 0 151 | 152 | def restart(self, data=None): 153 | """ Restart the timer """ 154 | self.reset() 155 | self.start() 156 | return self 157 | 158 | -------------------------------------------------------------------------------- /mudpi/extensions/dht/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | DHT Sensor Interface 3 | Connects to a DHT device to get 4 | humidity and temperature readings. 5 | """ 6 | import time 7 | 8 | import adafruit_dht 9 | import board 10 | 11 | from mudpi.constants import METRIC_SYSTEM 12 | from mudpi.extensions import BaseInterface 13 | from mudpi.extensions.sensor import Sensor 14 | from mudpi.logger.Logger import Logger, LOG_LEVEL 15 | from mudpi.exceptions import MudPiError, ConfigError 16 | 17 | 18 | class Interface(BaseInterface): 19 | 20 | def load(self, config): 21 | """ Load DHT sensor component from configs """ 22 | sensor = DHTSensor(self.mudpi, config) 23 | if sensor: 24 | self.add_component(sensor) 25 | return True 26 | 27 | def validate(self, config): 28 | """ Validate the dht config """ 29 | if not isinstance(config, list): 30 | config = [config] 31 | 32 | for conf in config: 33 | if not conf.get('pin'): 34 | raise ConfigError('Missing `pin` in DHT config.') 35 | 36 | if str(conf.get('model')) not in DHTSensor.models: 37 | conf['model'] = '11' 38 | Logger.log( 39 | LOG_LEVEL["warning"], 40 | 'Sensor Model Error: Defaulting to DHT11' 41 | ) 42 | 43 | return config 44 | 45 | 46 | class DHTSensor(Sensor): 47 | """ DHT Sensor 48 | Returns a random number 49 | """ 50 | 51 | # Number of attempts to get a good reading (careful to not lock worker!) 52 | _read_attempts = 3 53 | 54 | # Models of dht devices 55 | models = { 56 | '11': adafruit_dht.DHT11, 57 | '22': adafruit_dht.DHT22, 58 | '2302': adafruit_dht.DHT22 59 | } # AM2302 = DHT22 60 | 61 | """ Properties """ 62 | 63 | @property 64 | def id(self): 65 | """ Return a unique id for the component """ 66 | return self.config['key'] 67 | 68 | @property 69 | def name(self): 70 | """ Return the display name of the component """ 71 | return self.config.get('name') or f"{self.id.replace('_', ' ').title()}" 72 | 73 | @property 74 | def state(self): 75 | """ Return the state of the component (from memory, no IO!) """ 76 | return self._state 77 | 78 | @property 79 | def classifier(self): 80 | """ Classification further describing it, effects the data formatting """ 81 | return 'climate' 82 | 83 | @property 84 | def type(self): 85 | """ Model of the device """ 86 | return str(self.config.get('model', '11')) 87 | 88 | @property 89 | def read_attempts(self): 90 | """ Number of times to try sensor for good data """ 91 | return int(self.config.get('read_attempts', self._read_attempts)) 92 | 93 | """ Methods """ 94 | 95 | def init(self): 96 | """ Connect to the device """ 97 | self._sensor = None 98 | self.pin_obj = getattr(board, self.config['pin']) 99 | 100 | if self.type in self.models: 101 | self._dht_device = self.models[self.type] 102 | 103 | self.check_dht() 104 | 105 | return True 106 | 107 | def check_dht(self): 108 | """ Check if the DHT device is setup """ 109 | if self._sensor is None: 110 | try: 111 | self._sensor = self._dht_device(self.pin_obj) 112 | except Exception as error: 113 | Logger.log( 114 | LOG_LEVEL["error"], 115 | 'Sensor Initialize Error: DHT Failed to Init' 116 | ) 117 | self._sensor = None 118 | Logger.log( 119 | LOG_LEVEL["debug"], 120 | error 121 | ) 122 | return False 123 | return True 124 | 125 | def update(self): 126 | """ Get data from DHT device""" 127 | humidity = None 128 | temperature_c = None 129 | _attempts = 0 130 | 131 | if self.check_dht(): 132 | while _attempts < self.read_attempts: 133 | try: 134 | # Calling temperature or humidity triggers measure() 135 | temperature_c = self._sensor.temperature 136 | humidity = self._sensor.humidity 137 | except RuntimeError as error: 138 | # Errors happen fairly often, DHT's are hard to read 139 | Logger.log(LOG_LEVEL["debug"], error) 140 | except Exception as error: 141 | Logger.log( 142 | LOG_LEVEL["error"], 143 | f'DHT Device Encountered an Error. Attempt {_attempts+1}/{self.read_attempts}' 144 | ) 145 | self._sensor.exit() 146 | self._sensor = None 147 | 148 | if humidity is not None and temperature_c is not None: 149 | _temperature = temperature_c if self.mudpi.unit_system == METRIC_SYSTEM else (temperature_c * 1.8 + 32) 150 | readings = { 151 | 'temperature': round(_temperature, 2), 152 | 'humidity': round(humidity, 2) 153 | } 154 | self._state = readings 155 | return readings 156 | else: 157 | Logger.log( 158 | LOG_LEVEL["debug"], 159 | f'DHT Reading was Invalid. Attempt {_attempts+1}/{self.read_attempts}' 160 | ) 161 | time.sleep(2.1) 162 | _attempts +=1 163 | return None 164 | --------------------------------------------------------------------------------