diff options
author | erg_samowzbudnik <uinarf@autistici.org> | 2021-06-01 15:11:52 +0200 |
---|---|---|
committer | erg_samowzbudnik <uinarf@autistici.org> | 2021-06-01 15:11:52 +0200 |
commit | a2a05d9e9d9245be4c6e4e9c70334b8ac5fc299b (patch) | |
tree | b44a805bfcff41d82c8cfc19cb2e92cc4af1b56d | |
download | RPGH-a2a05d9e9d9245be4c6e4e9c70334b8ac5fc299b.tar.gz RPGH-a2a05d9e9d9245be4c6e4e9c70334b8ac5fc299b.tar.bz2 RPGH-a2a05d9e9d9245be4c6e4e9c70334b8ac5fc299b.zip |
Initial commit of a beta version.
-rw-r--r-- | RPGH.py | 345 | ||||
-rw-r--r-- | RPGH_gui.py | 71 | ||||
-rw-r--r-- | RPGH_gui.ui | 71 | ||||
-rw-r--r-- | monitor_unified.py | 516 | ||||
-rw-r--r-- | mplwidget.py | 41 | ||||
-rw-r--r-- | settings_client.cfg | 5 | ||||
-rw-r--r-- | settings_config_parser.cfg | 37 |
7 files changed, 1086 insertions, 0 deletions
@@ -0,0 +1,345 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "Franek Łazarewicz-Muradyan" +__copyright__ = "Copyright 2021, Franek Łazarewicz-Muradyan" +__version__ = "0.1" +__status__ = "Beta" +__email__ = "uinarf@autistici.org" + +""" +This is the main program starting the GUI +""" + +import sys, os +from PyQt5 import QtWidgets +import matplotlib +matplotlib.use('qt5agg') +from matplotlib.figure import Figure +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib import dates +from datetime import datetime, timedelta +from time import sleep +import configparser +from RPGH_gui import Ui_MainWindow +#from RPGH_main_gui import Ui_MainWindow +import pandas as pd +from pandas.plotting import register_matplotlib_converters +register_matplotlib_converters() +import mysql.connector + +__location__ = os.path.realpath( + os.path.join(os.getcwd(), os.path.dirname(__file__))) + +# read and extract/format configuration data +config_f = 'settings_config_parser.cfg' +config_cl = 'settings_client.cfg' +config_abs = os.path.join(__location__, config_f) +config_cl_abs = os.path.join(__location__, config_cl) +cfg = configparser.ConfigParser(interpolation=None) +cfg_cl = configparser.ConfigParser(interpolation=None) +cfg.read(config_abs) +cfg_cl.read(config_cl_abs) + + +class Data: + + def fetch_table(self, table, _min, _max): + self.table = table + # limits of data to fetch from-to: + self._min = _min + self._max = _max + try: + conn = mysql.connector.connect(**cfg_cl['mysql']) + except mysql.connector.Error as e: + try: + conn = mysql.connector.connect( + host=cfg_cl['mysql']['host'], + user=cfg_cl['mysql']['user'], + password=cfg_cl['mysql']['password'], + database=cfg_cl['mysql']['database'] + ) + except mysql.connector.Error as e: + print(f"Couldn't connect to a database: {e}") + sys.exit() + cur = conn.cursor() + sql = ( + f"SELECT * FROM {self.table} WHERE timestamp BETWEEN %s AND %s" + ) + #sql = ( + # f"SELECT * FROM {self.table};" + #) + cur.execute(sql, (self._min, self._max)) + #cur.execute(sql) + rows = cur.fetchall() + fields = [i[0] for i in cur.description] + df = pd.DataFrame([[x for x in i] for i in rows]) + + return fields, df + +class DesignerMainWindow(QtWidgets.QMainWindow, Ui_MainWindow): + def __init__(self, parent = None): + + super(DesignerMainWindow, self).__init__(parent) + self.setupUi(self) + +# setting up mplwidget + self.mplwidget() + + def mplwidget(self): + + timestamp_format = cfg.get('various', 'timestamp_format') + _now_p = datetime.now() + _past_p = _now_p - timedelta(hours=12) + # time limits formated for mysql: + _now = _now_p.strftime(timestamp_format) + _past = _past_p.strftime(timestamp_format) + +# unpacking data settings + temp_max = cfg.getint('temperature', 'temp_max') + temp_min = cfg.getint('temperature', 'temp_min') + light_max = cfg.getint('light', 'light_max') + light_min = cfg.getint('light', 'light_min') + humid_max = cfg.getint('humidity', 'humidity_max') + humid_min = cfg.getint('humidity', 'humidity_min') + soil_max = cfg.getint('water', 'soil_moisture_max') + soil_min = cfg.getint('water', 'soil_moisture_min') + + # ax limits + now = _now_p.strftime("%H:%M:%S") + past = _past_p.strftime("%H:%M:%S") + +# unpacking temperature data + _temp_data = Data() + temp_fields, temp_data = _temp_data.fetch_table( + 'temperature', _past, _now + ) + timestamp_temp = pd.to_datetime( + pd.Series(temp_data[0]) + ) + temp_01 = ( + temp_data[1], + cfg['w1_mapping'][f'{temp_fields[1]}'] + ) + temp_02 = ( + temp_data[2], + cfg['w1_mapping'][f'{temp_fields[2]}'] + ) + temp_03 = ( + temp_data[3], + cfg['w1_mapping'][f'{temp_fields[3]}'] + ) + temp_04 = ( + temp_data[4], + cfg['w1_mapping'][f'{temp_fields[4]}'] + ) + +# unpacking light data + self.light_data = Data() + light_fields, light_data = self.light_data.fetch_table( + 'light', _past, _now) + timestamp_1 = pd.to_datetime( + pd.Series(light_data[0]), format=timestamp_format + ) + broadband = light_data[1] + infrared = light_data[2] + visible = light_data[3] + +# unpacking humidity data + self.hum_data = Data() + hum_fields, hum_data = self.hum_data.fetch_table( + 'humidity', _past, _now + ) + timestamp_2 = pd.to_datetime( + pd.Series(hum_data[0]), format=timestamp_format + ) + humid = hum_data[2] + +# unpacking rain data + self.rain_data = Data() + rain_fields, rain_data = self.hum_data.fetch_table( + 'rain', _past, _now + ) + timestamp_3 = pd.to_datetime( + pd.Series(rain_data[0]), format=timestamp_format + ) + rain = rain_data[1] + + days = dates.DayLocator() + hours = dates.HourLocator() + minutes = dates.MinuteLocator() + seconds = dates.SecondLocator() + dfmt = dates.DateFormatter('%b %d') + tmfd = dates.DateFormatter('%H %M') + +# ploting temperature data + self.mpl.canvas.ax.set_ylabel('Temperature ($^\circ$C)') + self.mpl.canvas.ax.grid(True) + self.mpl.canvas.ax.plot( + timestamp_temp, + temp_01[0], + c='brown', + ls=':', + label=f'{temp_01[1]}' + ) + self.mpl.canvas.ax.plot( + timestamp_temp, + temp_02[0], + c='orange', + ls='dotted', + label=f'{temp_02[1]}' + ) + self.mpl.canvas.ax.fill_between( + timestamp_temp, + temp_01[0], + temp_02[0] + ) + self.mpl.canvas.ax.fill_between( + timestamp_temp, + temp_01[0], + temp_max, + where=temp_01[0]>=temp_max, + edgecolor='red', + facecolor='none', + hatch='/', + interpolate=True + ) + self.mpl.canvas.ax.fill_between( + timestamp_temp, + temp_02[0], + temp_min, + where=temp_02[0]<=temp_min, + edgecolor='red', + facecolor='none', + hatch='/', + interpolate=True + ) + self.mpl.canvas.ax.legend(loc='upper left') + #self.mpl.canvas.ax.set_xlim([now, past]) +# adding light subplot + self.mpl.canvas.ax_1.set_ylabel('Light') + self.mpl.canvas.ax_1.grid(True) + self.mpl.canvas.ax_1.plot( + timestamp_1, + broadband, + c='cyan', + ls='-.', + label='broadband' + ) + self.mpl.canvas.ax_1.plot( + timestamp_1, + infrared, + c='red', + ls='-.', + label='infrared' + ) + self.mpl.canvas.ax_01 = self.mpl.canvas.ax_1.twinx() + self.mpl.canvas.ax_01.set_ylabel("light lm") + self.mpl.canvas.ax_01.plot( + timestamp_1, + visible, + c='orange',ls='-.', + label='visible' + ) + self.mpl.canvas.ax_1.legend(loc='upper left') + self.mpl.canvas.ax_01.legend(loc='lower left') + self.mpl.canvas.ax_1.fill_between( + timestamp_1, + broadband, + light_max, + where=broadband>=light_max, + edgecolor='red', + facecolor='none', + hatch="/", + interpolate=True + ) + self.mpl.canvas.ax_1.fill_between( + timestamp_1, + broadband, + light_min, + where=broadband<=light_min, + edgecolor='red', + facecolor='none', + hatch='/', + interpolate=True + ) + #self.mpl.canvas.ax_1.set_xlim([now, past]) +# adding humidity subplot + self.mpl.canvas.ax_2.set_ylabel('Humidity %') + self.mpl.canvas.ax_2.set_xlabel('time') + self.mpl.canvas.ax_2.grid(True) + self.mpl.canvas.ax_2.plot( + timestamp_2, + humid, + c='blue', + ls='--', + label='humidity' + ) + self.mpl.canvas.ax_2.legend(loc='upper left') + #self.mpl.canvas.ax_2.set_xlim([now, past]) + +# setting up weather station tab: +# adding temperature plot + self.mpl1.canvas.ax.set_ylabel('Temperature ($^\circ$C)') + self.mpl1.canvas.ax.grid(True) + self.mpl1.canvas.ax.plot( + timestamp_temp, + temp_03[0], + c='brown', + ls=':', + label=f'{temp_03[1]}' + ) + self.mpl1.canvas.ax.plot( + timestamp_temp, + temp_04[0], + c='orange', + ls='dotted', + label=f'{temp_04[1]}' + ) + self.mpl1.canvas.ax.legend(loc='upper left') + #self.mpl1.canvas.ax.set_xlim([now, past]) +# adding light plot + self.mpl1.canvas.ax_1.set_ylabel('Light lm') + self.mpl1.canvas.ax_1.grid(True) + self.mpl1.canvas.ax_1.plot( + timestamp_1, + visible, + c='yellow', + label='light' + ) + self.mpl1.canvas.ax_1.legend(loc='upper left') + #self.mpl1.canvas.ax_1.set_xlim([now, past]) +# adding humidity/rainfall graph +# mind you that hist() doesn't do what I'd like it to do. redo. + self.mpl1.canvas.ax_2.set_ylabel('Rainfall mm') + self.mpl1.canvas.ax_2.set_xlabel('time') + self.mpl1.canvas.ax_2.grid(True) +# this is a hack: unit for bar width is days hence hum_data.shape[0] = numbers +# of columns in dataframe allowing for dynamic sizing + self.mpl1.canvas.ax_2.bar( + timestamp_3, + rain, + width = .01*(1/rain_data.shape[0]), + edgecolor = 'black', + label='rainfall' + ) + self.mpl1.canvas.ax_2.legend(loc='upper left') + #self.mpl1.canvas.ax_2.set_xlim([now, past]) + + + def mpl_replot(self): + self.mpl.canvas.ax.clear() + self.mpl.canvas.ax_1.clear() + self.mpl.canvas.ax_2.clear() + self.mpl1.canvas.ax.clear() + self.mpl1.canvas.ax_1.clear() + self.mpl1.canvas.ax_2.clear() + self.mpl.canvas.draw_idle() + self.mpl1.canvas.draw_idle() + self.mplwidget() + +if __name__ == '__main__': + app = QtWidgets.QApplication([sys.argv]) + gui = DesignerMainWindow() + gui.show() + sys.exit(app.exec_()) diff --git a/RPGH_gui.py b/RPGH_gui.py new file mode 100644 index 0000000..eb5f38e --- /dev/null +++ b/RPGH_gui.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "Franek Łazarewicz-Muradyan" +__copyright__ = "Copyright 2021, Franek Łazarewicz-Muradyan" +__version__ = "0.1" +__status__ = "Beta" +__email__ = "uinarf@autistici.org" +# Form implementation generated from reading ui file 'RPGH_minimal_scaling_gui.ui' +# +# Created by: PyQt5 UI code generator 5.15.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(847, 526) + self.centralwidget = QtWidgets.QWidget(MainWindow) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) + self.centralwidget.setSizePolicy(sizePolicy) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout.setObjectName("horizontalLayout") + self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) + self.tabWidget.setObjectName("tabWidget") + self.mpl_tab = QtWidgets.QWidget() + self.mpl_tab.setObjectName("mpl_tab") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.mpl_tab) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.mpl = MplWidget(self.mpl_tab) + self.mpl.setObjectName("mpl") + self.verticalLayout_2.addWidget(self.mpl) + self.tabWidget.addTab(self.mpl_tab, "") + self.mpl1_tab = QtWidgets.QWidget() + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.mpl1_tab.sizePolicy().hasHeightForWidth()) + self.mpl1_tab.setSizePolicy(sizePolicy) + self.mpl1_tab.setObjectName("mpl1_tab") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.mpl1_tab) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.mpl1 = MplWidget(self.mpl1_tab) + self.mpl1.setObjectName("mpl1") + self.verticalLayout_3.addWidget(self.mpl1) + self.tabWidget.addTab(self.mpl1_tab, "") + self.horizontalLayout.addWidget(self.tabWidget) + MainWindow.setCentralWidget(self.centralwidget) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.retranslateUi(MainWindow) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "RPGH")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.mpl_tab), _translate("MainWindow", "Greenhouse")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.mpl1_tab), _translate("MainWindow", "Weather")) +from mplwidget import MplWidget diff --git a/RPGH_gui.ui b/RPGH_gui.ui new file mode 100644 index 0000000..d7759dd --- /dev/null +++ b/RPGH_gui.ui @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>847</width> + <height>526</height> + </rect> + </property> + <property name="windowTitle"> + <string>RPGH</string> + </property> + <widget class="QWidget" name="centralwidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="mpl_tab"> + <attribute name="title"> + <string>Greenhouse</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="MplWidget" name="mpl" native="true"/> + </item> + </layout> + </widget> + <widget class="QWidget" name="mpl1_tab"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <attribute name="title"> + <string>Weather</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="MplWidget" name="mpl1" native="true"/> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QStatusBar" name="statusbar"/> + </widget> + <customwidgets> + <customwidget> + <class>MplWidget</class> + <extends>QWidget</extends> + <header>mplwidget</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/monitor_unified.py b/monitor_unified.py new file mode 100644 index 0000000..83b5f63 --- /dev/null +++ b/monitor_unified.py @@ -0,0 +1,516 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "Franek Łazarewicz-Muradyan" +__copyright__ = "Copyright 2021, Franek Łazarewicz-Muradyan" +__version__ = "0.1" +__status__ = "Beta" +__email__ = "uinarf@autistici.org" + +""" +Program intended to be run as daemon reading from sensors and +writing to database. +""" + +import sys, os, glob, re +from time import strftime, localtime, sleep +import subprocess +import importlib +import pigpio +import smbus +import configparser +import mysql.connector +from mysql.connector import errorcode +# below installed on my system with custom ebuild, available through pip +import pigpio_dht +# we'll use it for now: + +#sys.path.append('..') + +#LOCAL_DIR = os.path.dirname(os.path.realpath(__file__)) + "/" +__location__ = os.path.realpath( + os.path.join(os.getcwd(), os.path.dirname(__file__))) +# here goes our settings file +settings_file = 'settings_config_parser.py' +settings_db = 'settings_server.cfg' + +# setting up configparser +# so that paths work when script is run from different directory +settings = os.path.join(__location__, settings_file) +settings_db = os.path.join(__location__, settings_db) +cfg = configparser.ConfigParser(interpolation=None) +cfg_db = configparser.ConfigParser(interpolation=None) +cfg.read(settings) +cfg_db.read(settings_db) + +class Sensor: + def __init__(self): + # read default database name + try: + self.dtb = cfg_db['mysql']['database'] + except KeyError: + self.dtb = 'default.dtb' + # here we'll defind the elusive NaN + # needs to be None for database + self.nan = None + self.timestamp_format = cfg.get('various', 'timestamp_format') + + def create_connection(self): + """ + Creates connection to database with credentials provided in + config file. + """ + self.conn = None + try: + self.conn = mysql.connector.connect(**cfg_db['mysql']) + except mysql.connector.Error as e: + if e.errno == errorcode.ER_ACCESS_DENIED_ERROR: + print("Something is wrong with your username and password.") + else: + print(e) + return self.conn + + def create_database(cursor): + try: + cursor.execute( + f"CREATE DATABASE {self.dtb} DEFAULT CHARACTER SET 'utf-8'" + ) + except mysql.connector.Error as e: + print(f"Failed creating database: {e}") + + def create_table(self, t_name, dictionary): + """ + Takes table name (w1, light etc) and data in shape of a dictionary + and creates database table. + """ + self.t_name = t_name + self.data = dictionary + self.cur = self.create_connection().cursor() + try: + self.cur.execute(f"USE {self.dtb}") + except mysql.connector.Error as e: + print(f"Database {dtb} does not exist.") + create_database(self.cur) + self.cur.execute(f"USE {self.dtb}") + # Create db statement + # Achtung: identifiers cannot start with digits!!! + # Achtung: identifiers cannot contain special characters (-_-)!!! + # This: row name + data type + rows_tp= ['timestamp timestamp'] + # This: row name + self.rows = ['timestamp'] + for i in self.data.keys(): + # skip timestamp + if i != 'timestamp': + # digit cannot go first + if i[0].isdigit(): + i = 'sens'+i + # strip of special characters excluding ',' and ' ' + # and remove ', ' at the end + i = re.sub('[^A-Za-z0-9, ]+', '', i) + # all the sensor data is float, ok? + rows_tp.append(i + ' float') + self.rows.append(i) + rows_tp = (', ').join(rows_tp) + self.rows = (', ').join(self.rows) + stmt = ( + f"CREATE TABLE IF NOT EXISTS {self.t_name} (" + f"{rows_tp}" + ")" + ) + try: + self.cur.execute(stmt) + except mysql.connector.Error as e: + print(e) + + def db_write(self, t_name, dictionary): + """ + Write dictionary containing timestamp and sensor values into + a table in a database. + """ + self.data = dictionary + self.cur = self.create_connection().cursor() + #cur = conn.cursor() + self.create_table(t_name, dictionary) + + vals = [*self.data.values()] + # placeholder length + pl_hd = ('%s,'*len(self.data))[:-1] + sql = ( + f"INSERT INTO {self.t_name} (" + f"{self.rows})" + "VALUES (" + f"{pl_hd}" + ")" + ) + try: + #cur.execute(sql, vals) + self.cur.execute(sql, vals) + except mysql.connector.Error as e: + print(e) + self.conn.commit() + self.cur.close() + self.conn.close() + + + def write(self, data, log_file): + """ + Takes data in form of dictionary and writes to a log file. + Data in dictionary takes form of sensor name : value pairs. + In case of sensors returning multiple values each sensor name + is followed by data type (i.e. 'tsl_0-infrared'). + """ + # Since I'm replacing it with db_write it has been abandoned, + # variables passed from dictionary need stringification (sic!) + # employed one hasn't been checked. + # leaving it here, may get used as a fallback option. + self.data = data + self.log_file = log_file + while True: + with open(self.log_file, 'a') as f: + [_data.extend(str(x)) for x in self.data.items()] + data = ' '.join(_data) + f.write(f'{data}\n') + break + +class Temperature(Sensor): + """ + Dealing with 1 wire temperature sensors. + """ + w1_path = '/sys/bus/w1/devices' + the_line = w1_path + '/28-*/w1_slave' + sens_files = glob.glob(the_line) + + def __init__(self): + """ + Check if w1_gpio and w1_therm kernel modules are loaded. + """ + # implement writing warnings and such into a log file + super().__init__() + cmd = ['lsmod'] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + output, error = proc.communicate() + if 'w1_gpio' in output.decode(): + pass + else: + print('Module w1-gpio not found.') + # write that to a log file + if 'w1_therm' in output.decode(): + pass + else: + print('Module w1-therm not found.') + # write that to a log file + + def check_w1(self): + """ + Checks consistency of w1 sensor mapping. Need to have access to the + database already + """ + conn = self.create_connection() + cur = conn.cursor() + try: + dtb = cfg_db['mysql']['database'] + except KeyError: + print("You have not defined database. Check your settings file.") + sys.exit() + cur.execute(f"USE {dtb};") + sql = ( + "SELECT * FROM temperature;" + ) + cur.execute(sql) + # checking actual number of w1 sensors detected: + act_sens_list = [] + [[act_sens_list.append(i) for i in x if i.__contains__('28-')] \ + for z, x, y in os.walk(self.w1_path)] + # checking number of sensors in database: + db_sens_list = [i[0] for i in cur.description][1:] + # checking for no mapping defined in config file: + if len(cfg['w1_mapping']) == 0: + print("Info: you have no mapping for w1 temperature sensors " + "defined, assigning default mapping. You can reassing them " + "in settings file." + ) + map_list = ['Inside ground', 'Inside', 'Outside ground', 'Outside'] + cfg['w1_mapping'] = dict(zip(db_sens_list , map_list)) + with open(settings_file, 'w') as f: + cfg.write(f) + elif len(cfg['w1_mapping']) != 0 and len(db_sens_list) > len(act_sens_list): + print("Warning: Number of w1 temperature sensors detected is " + f"({len(act_sens_list)}), smaller than number of sensors in " + f"the database ({len(db_sens_list)}). " + "Perhaps a sensor has detached/malfunctioned?" + ) + sys.exit() + elif len(db_sens_list) < len(act_sens_list): + print("Warning: number of w1 temperature sensors detected is " + "greater than that in a database." + ) + sys.exit() + + def read(self): + """ + Returns dictionary of available sensor names as keys and readings in + Celcius as values. + """ + + sens_list = ['timestamp'] + [[sens_list.append(i) for i in x if i.__contains__('28-')] \ + for z, x, y in os.walk(self.w1_path)] + temp_list = [strftime(self.timestamp_format, localtime())] + for i in self.sens_files: + with open(i, 'r') as f: + tries=0 + _crc = f.readline()[-4:-1] + if _crc !='YES': + reading = self.nan + else: + reading = f.readline().split('t=')[1] + try: + temp = str(round(float(reading)/1000, 1)) + except ValueError: + temp = self.nan + temp_list.append(temp) + +# morphing into a dict so that we knew which sensor is which + temp_dict = dict(zip(sens_list, temp_list)) + return temp_dict + +class Humidity(Sensor): + """ + This part deals with a DHT11 temperature and humidity sensor. + Using pigpio-dht module so not much to do here + """ + def __init__(self): + super().__init__() + self.pin = cfg.getint('hardware_settings','dht_pin_1') + self.dht = pigpio_dht.DHT11(self.pin) + + def read(self, n=0): + """ + Reading from dht11 sensor, tries to read 10x on bad read then passes. + """ + n=0 + now = strftime(self.timestamp_format, localtime()) + reading = {'timestamp': now} + try: + reading.update(self.dht.read()) + except TimeoutError: + reading.update({'temp_c': self.nan, 'humidity': self.nan}) + return reading + while True: + if reading['valid'] == True: + del reading['temp_f'] + del reading['valid'] + for i in list(reading): + reading[i] = str(reading[i]) + return reading + break + if reading['valid'] == False and n >=3: + del reading['temp_f'] + del reading['valid'] + reading.update({'temp_c': self.nan, 'humidity': self.nan}) + return reading + break + else: + n+=1 + sleep(0.17) + +class Light(Sensor): + """ + This part deals with tsl2561 sensor using smbus. + """ + # at the moment I find changing gain and exposure time to not be reliable + # and/or usefull. May try to reimplement it at a later date. + _SMBUSADDR = 1 + + tsls = [ + ('_TSLADDR', 0x39), # default + ('_TSLADDR_LOW', 0x29), + ('_TSLADDR_HIGH', 0x49) + ] + + # controls + # this first is for on/of only + _REG_CONTR = 0x00 + # this is for any other commands we'll use here + _CONTR = 0x01 + # command option + _CMD = 0x80 + + # max reading a channel can hold + max_read = 65535 + + # commands + _ON = 0x03 + _OF = 0x00 + # set gain + _CMD_GN = 0x81 + + # registers + _regs = { + '_CH_0' : 0xAC, # visible + infra red + '_CH_1' : 0xAE # infra red + } + # gain and exposure + _gain = [ + ('_GN_VLOW', 0x00), # gain 1x, exposure : 13.7ms + ('_GN_LOW', 0x01), # gain 1x, exposure : 100ms + ('_GN_DEF', 0x02), # gain 1x, exposure : 402ms + ('_GN_HI', 0x10), # gain 16x, exposure : 13.7ms + ('_GN_VHI', 0x11), # gain 16x, exposure : 100ms + ('_GN_UHI', 0x12) # gain 16x, exposure : 402ms + ] + + def __init__(self, tsl): + super().__init__() + self.tsl = tsl + if self.tsl > 2: + print('We can only adress 3 TSL2561 sensors on this bus, aborting') + sys.exit() + # since default gain is always _gain[2][1] + self.n = 2 + # initiate bus + self.bus = smbus.SMBus(self._SMBUSADDR) + # turn on sensor + self.tsl_addr = self.tsls[self.tsl][1] + self.bus.write_byte_data(self.tsl_addr, self._REG_CONTR | self._CMD, self._ON) + + def setGain(self, gain): + """ + To be used from within read function. + """ + self.gain = gain + self.bus.write_byte_data(self.tsl_addr, self._CMD_GN | self._CMD, gain) + + def read(self, idx=0): + # Upon testing sensor I've come to the conclusion that all that + # setting gain and timing gives me nothing, sensor reports limits + # on those instead. + self.idx = idx + _vals = [] + _iter = 0 + for i in self._regs.keys(): + _vals.append(self.bus.read_word_data(self.tsl_addr, self._regs.get(i))) + #for i in _vals: + # if self.safe_read < i < self.max_read: + # pass + # elif i >= self.max_read and self.idx < 3: + # idx+=1 + # # if excessive reading decrease gain/exposure + # self.n -=1 + # try: + # gain = self._gain[self.n][1] + # self.setGain(gain) # increment gain position by one + # self.read(self.idx) + # except IndexError: + # _vals = [self.nan, self.nan] + # elif 0 <= i <= self.safe_read: + # # attempt to increase gain/exposure to optimal + # idx+=1 + # if idx >= 3: + # pass + # else: + # try: + # self.n +=1 + # gain = self._gain[self.n][1] + # self.setGain(gain) + # self.read(self.idx) + # except IndexError: + # pass + # else: + # _vals = ['Nan', 'Nan'] + lux = self.calculate_lux(_vals) + now = strftime(self.timestamp_format, localtime()) + reading = { + 'timestamp': now, + f'tsl_{self.tsl}-Broadband': _vals[0], + f'tsl_{self.tsl}-Infrared': _vals[1], + f'tsl_{self.tsl}-Lux': lux + } + return reading + + def calculate_lux(self, _vals): + """ + Calculate lux from tuple 'visible+infrared'/'infrared' as returned + by read() function. From datasheet for FN package sensor version. + """ + ch0, ch1 = _vals + if ch0 == 0: + lux = 0 + elif ch0 == self.nan or ch1 == self.nan: + lux = self.nan + else: + ratio = ch1/ch0 + if 0 < ratio <= 0.52: + lux = 0.0304 * ch0 - 0.062 * ch0 * (ratio ** 1.4) + elif 0.5 < ratio <= 0.61: + lux = 0.0224 * ch0 - 0.031 * ch1 + elif 0.61 < ratio <= 0.8: + lux = 0.0128 * ch0 - 0.0153 * ch1 + elif 0.8 < ratio <= 1.3: + lux = 0.00146 * ch0 - 0.00112 * ch1 + else: + lux = 0 + lux = round(lux, 4) + return lux + +class Rain(Sensor): + + def __init__(self): + super().__init__() + self.pin = cfg.getint('hardware_settings', 'rain_pin') + self.pi = pigpio.pi() + self.pi.set_pull_up_down(self.pin, pigpio.PUD_UP) + self.rain = self.pi.callback(self.pin) + self.bucket = cfg.getfloat('hardware_settings', 'bucket_size') + + def read(self): + """ + Read and return rain data. + """ + amount = str(round(self.rain.tally() * self.bucket, 4)) + now = strftime(self.timestamp_format, localtime()) + data = { + 'timestamp': now, + 'amount' : amount + } + self.rain.reset_tally() + + return data + +# GPIO.add_event_detect(self.pin, GPIO.FALLING, callback=self.read(), +# bouncetime=300) + +if __name__ == '__main__': + fq = cfg.getint('hardware_settings', 'read_frequency') + print("Instantiating gpio connection") + dht = Humidity() + tmp = Temperature() + tsl = Light(0) +# tsl_sensors = [] +# tsl_no = cfg.getint('hardware_settings', 'tsl2561_number') +# for i in range(tsl_no): +# tsl_sensors.append(Light(i)) + rain = Rain() + while True: + #if not pi.connected: + # try: + # print('Attempting to reconnect to pigpiod ...') + # pi = pigpio.pi() + # except: + # print("Pigpio couldn't connect to pi on port 8888") + # exit() + #else: + # pass + tmp.check_w1() + tmp.db_write('temperature', tmp.read()) + dht.db_write('humidity', dht.read()) + tsl.db_write('light', tsl.read()) + rain.db_write('rain', rain.read()) + #for i in tsl_sensors: + # i.log = f'{log_files[light_log]}_{i}' + # i.write(i.read(), i.log) + #pi.stop() + sleep(fq) + + sys.exit() diff --git a/mplwidget.py b/mplwidget.py new file mode 100644 index 0000000..df3a054 --- /dev/null +++ b/mplwidget.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +__author__ = "Franek Łazarewicz-Muradyan" +__copyright__ = "Copyright 2021, Franek Łazarewicz-Muradyan" +__version__ = "0.1" +__status__ = "Beta" +__email__ = "uinarf@autistici.org" + +from PyQt5 import QtWidgets +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as \ +NavigationToolbar +from matplotlib.figure import Figure +from matplotlib import gridspec + +#matplotlib.use('QT5Agg') + +class MplCanvas(FigureCanvas): + def __init__(self): + self.fig = Figure(tight_layout=True) + grid = gridspec.GridSpec(ncols=32, nrows=3) + self.ax = self.fig.add_subplot(grid[0, 0:32]) + self.ax_1 = self.fig.add_subplot(grid[1, 0:32]) + self.ax_2 = self.fig.add_subplot(grid[2, 0:32]) + self.fig.align_labels() + FigureCanvas.__init__(self, self.fig) + FigureCanvas.setSizePolicy(self, + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + FigureCanvas.updateGeometry(self) + +class MplWidget(QtWidgets.QWidget): + def __init__(self, parent = None): + QtWidgets.QWidget.__init__(self, parent) + self.canvas = MplCanvas() + self.toolbar = NavigationToolbar(self.canvas, self) + self.vbl = QtWidgets.QVBoxLayout() + self.vbl.addWidget(self.toolbar) + self.vbl.addWidget(self.canvas) + self.setLayout(self.vbl) diff --git a/settings_client.cfg b/settings_client.cfg new file mode 100644 index 0000000..51a728f --- /dev/null +++ b/settings_client.cfg @@ -0,0 +1,5 @@ +[mysql] +host = yourip +user = youruser +password = yourpassword +database = yourdatabase diff --git a/settings_config_parser.cfg b/settings_config_parser.cfg new file mode 100644 index 0000000..a751fcc --- /dev/null +++ b/settings_config_parser.cfg @@ -0,0 +1,37 @@ +[temperature] +temp_max = 31 +temp_min = 5 + +[humidity] +humidity_max = 35 +humidity_min = 20 + +[light] +light_min = 200 +light_max = 100 + +[hardware_settings] +read_frequency = 15 +dht_pin_1 = 17 +dht_pin_2 = None +dht_type_1 = dht11 +dht_type_2 = None +tsl2561_pin = None +tsl2561_number = 1 +rain_pin = 6 +bucket_size = 0.2794 +w1_pin = 4 +soil_moisture_sensors_number = None + +[w1_mapping] + +[logs] +temperature = sensor_logs/temp.dat +light = sensor_logs/light.dat +humidity = sensor_logs/humidity.dat +rain = sensor_logs/rain.dat +monitor = monitor.log + +[various] +timestamp_format = %Y-%m-%d %H:%M:%S +nan = None |