aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerg_samowzbudnik <uinarf@autistici.org>2021-06-01 15:11:52 +0200
committererg_samowzbudnik <uinarf@autistici.org>2021-06-01 15:11:52 +0200
commita2a05d9e9d9245be4c6e4e9c70334b8ac5fc299b (patch)
treeb44a805bfcff41d82c8cfc19cb2e92cc4af1b56d
downloadRPGH-a2a05d9e9d9245be4c6e4e9c70334b8ac5fc299b.tar.gz
RPGH-a2a05d9e9d9245be4c6e4e9c70334b8ac5fc299b.tar.bz2
RPGH-a2a05d9e9d9245be4c6e4e9c70334b8ac5fc299b.zip
Initial commit of a beta version.
-rw-r--r--RPGH.py345
-rw-r--r--RPGH_gui.py71
-rw-r--r--RPGH_gui.ui71
-rw-r--r--monitor_unified.py516
-rw-r--r--mplwidget.py41
-rw-r--r--settings_client.cfg5
-rw-r--r--settings_config_parser.cfg37
7 files changed, 1086 insertions, 0 deletions
diff --git a/RPGH.py b/RPGH.py
new file mode 100644
index 0000000..bfbad39
--- /dev/null
+++ b/RPGH.py
@@ -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