Problem bei meinem grow box - Projekt

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

Hallo allerseits,
ich versuche gerade mit dem Raspberry Pi 3B+ umzusetzen, was ich zuvor schon mal mit einer semiprofessionellen SPS entwickelt und programmiert habe.
Dass Python anders funktioniert, ist mir klar. Ich lerne gerade Schritt für Schritt dazu.

Das Projekt ist eine vollautomatische grow box, die meine Tomaten-, Paprika- und Pepperonipflanzen ab dem späten Winter kommenden Jahres behüten soll.

Der Sachstand ist folgender: ich messe Bodenfeuchte, Lufttemperatur und Feuchte, Lichtstärke, den Füllstand eines Wasserbehälters und den Durchfluss. Ich steuere eine Pumpe, einen Ventilator, eine Heizung und Pflanzenleuchten. Das alles mit Hilfe eines Relais-Moduls.

Soweit so gut, grundsätzlich funktioniert das alles in einem Script in Echtzeit, mit entsprechenden Grenzwerten für an und aus.
Nun reagieren aber die Bodenfeuchtsensoren äußerst träge. Würde ich bei Grenzwert "trocken" die Pumpe bis Grenzwert "feucht genug" laufen lassen, würde ich das kleine Anzuchtbeet überfluten.
Ich will daher folgendes Verfahren anwenden: wenn Grenzwert "trocken" unterschritten wird, soll die Pumpe für eine definierte Zeit laufen und dann stoppen. Anschließend soll z. B. 15 Minuten abgewartet werden, um erneut den Soll- / Istwert Vergleich durchzuführen. Ist der Sollwert "feucht genug erreicht", bleibt die Pumpe aus, wenn nicht, fördert sie noch eine Portion Wasser.
Nun kann ich allerdings nicht das ganze Skript für 15 Minuten pausieren, da die anderen Prozesse in Echtzeit weiter laufen sollen. Mit der SPS ist das kein Problem, das die Prozesse, anders als bei Python, quasi parallel laufen und das Problem keines ist, da hier ein simpler Timer (delay) Abhilfe schafft.

Sollte jemand von euch eine Idee dazu haben, würde ich mich sehr freuen davon zu erfahren. Da ich noch Lernender bin, wäre vielleicht ein Beispiel hilfreich.
Besten Dank im Voraus.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Crossover: Man könnte beispielsweise eine Variable einführen in der man sich merkt wann die 15 Minuten begonnen haben und die immer prüfen bevor man misst, ob wirklich gemessen werden soll.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Oder du investierst 50€ in Codesys für den PI, und kannst wie gewohnt SPS programmieren.
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

@ _blackjack_: DU bringst mich auf eine Idee. Ich habe sowieso ein RTC-Modul um immer die korrekte Uhrzeit zu erhalten. Damit will ich die Beleuchtung tageszeitabhängig steuern. Vielleicht kann ich mit der Uhrzeit etwas programmieren um den Messintervall zu steuern. Da ich das Projekt noch mit TKinter visualisieren will, hat das den Nachteil, dass auch das Display den Wert nur in dem Zeitintervall aktualisiert. Aber das wäre erst die nächste Baustelle. Wie kann ich in Python die Uhrzeit verarbeiten? Das wäre dann die nächste Frage. Kann /muss man die in Integer oder Hex umwandeln um sie in eine Variable zu schreiben?

@_deeets_: Danke für den Hinweis. Davon hatte ich schon mal was gelesen und mir Codesys gerade mal angeschaut. Ich habe mir vorgenommen Python zu lernen und experimentiere schon seit ein paar Monaten, aus Zeitgründen allerdings nicht sehr intensiv. Dabei sind mir aber so viele Ideen eingefallen, was man damit und dem Pi alles machen kann, so dass ich nicht davon ablassen will und kann, weil es mit der Soft-SPS nicht zu realisieren wäre. Codesys wäre für meine grow box wohl schon das Richtige und der einfachere Weg. Ich überlege, ob ich mir das parallel dazu mal anschaue.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Für Datums- und Zeitberechnungen gibt es in der Python-Standardbibliothek das `datetime`-Modul. Und für so etwas wie Sachen zu bestimmten Zeiten zu machen solltest Du mal einen Blick auf das APScheduler-Package werfen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

Soweit ist es geschafft. Die Steuerung läuft vollständig mit Lüfter, Heizung, Beleuchtung und Bewässerungspumpe, Licht-, Bodenfeuchte-, Temperatur- und Luftfeuchtesensor in nur einem Python script. Die Durchflussmessung fliegt raus, weil sie für den geringen Durchfluss ungeeignet ist. Ich arbeite an einer Füllstandmessung für den Wasserbehälter mit Ultraschall.
Die Betriebszustände und Messwerte gebe ich momentan in der shell via Print aus. Das ist unkomfortabel und die shell fängt irgendwann an zu meckern.

Wäre da meine 2. Aufgabenstellung. Die Darstellung der Messwerte, GPIO-out Zustände und Anforderungen für zeitversetzte Aktionen (die Pumpe soll für eine definierte Zeit Wasser geben und dann eine definierte Zeit abwarten ob die Feuchtesensoren die Notwendigkeit einer erneuten Wassergabe signalisieren und pumpen oder eben nicht).

Ich möchte diese ganzen Information auf einem Display darstellen. Mit TKinter habe ich ein wenig gespielt, habe aber noch kein Konzept, wie ich die Informationen aus meinem Python script (Steuerprogramm) in ein TKinter script übertragen, bzw mit dem TKinter script holen kann, das parallel zum Steuerprogamm läuft. Oder kann ich TKinter auch in meinem Steuerprogramm integirieren?

Könnt ihr mir da etwas Anschub geben?
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Du brauchst einen Thread für die Steuerung und den Hauptthread für die GUI.
Und ich habe noch nie eine shell gesehen, die wergen eines Prints meckert.
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

Ich habe noch ein Problem mit dem Steuerungsscript und finde keine Lösung.
Für die Dauerschleife musste ich Try mit Main für die Verwendung des Helligkeitssensors ersetzen. Sobald der Helligkeitssensor einen Wert > 0 ausgibt, bekomme ich bei der Thonny IDE sowie im Linux Terminal, wenn ich das script dort starte, jede Menge Fehlermeldungen. Kann mir da jemand weiter helfen?

Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1171, in wrapper
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1158, in wrapper
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1232, in _execute_prepared_user_code
File "/home/pi/Gewächshaus/Gewächshaus2.py", line 181, in <module>
File "/home/pi/Gewächshaus/Gewächshaus2.py", line 99, in main
File "/home/pi/Gewächshaus/Gewächshaus2.py", line 69, in __init__
OSError: [Errno 24] Too many open files

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1219, in execute_source
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1176, in wrapper
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1060, in _prepare_user_exception
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 948, in _export_stack
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1021, in _get_frame_source_info
File "/usr/lib/python3/dist-packages/thonny/backend.py", line 2503, in _fetch_frame_source_info
File "/usr/lib/python3.5/tokenize.py", line 454, in open
OSError: [Errno 24] Too many open files: '/home/pi/Gewächshaus/Gewächshaus2.py'

Hier folgt das script. Die auskommentierten Zeilen rühren von meinen zahlreichen Versuchen her, den Fehler zu beseitigen:

#!/usr/bin/python3
#Gewächshaus2
import sys
from spidev import SpiDev
import smbus
import time
from time import sleep
from RPi import GPIO
import Adafruit_DHT
sensor = Adafruit_DHT.DHT22
pin = 12
#Helligkeit
DEVICE = 0x23
POWER_DOWN = 0x00
POWER_ON = 0x01
RESET = 0x07
CONTINUOUS_HIGH_RES_MODE_1 = 0x10
#ONE_TIME_HIGH_RES_MODE_1 = 0x20
bus = smbus.SMBus(1)
def convertToNumber(data):
result=(data[1] + (256 * data[0])) / 1.2
return (result)

def readLight(addr=DEVICE):
# Read data from I2C interface
#data = bus.read_i2c_block_data(addr,ONE_TIME_HIGH_RES_MODE_1)
data = bus.read_i2c_block_data(addr,CONTINUOUS_HIGH_RES_MODE_1)
return convertToNumber(data)

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
Ventilator = 5
Heizung = 6
Pumpe = 13
Lampe = 19
GPIO.setup (Ventilator, GPIO.OUT)
GPIO.setup (Heizung, GPIO.OUT)
GPIO.setup (Pumpe, GPIO.OUT)
GPIO.setup (Lampe, GPIO.OUT)

#Grenzwerte
SOLL_TEMP_HIGH = 23
SOLL_TEMP_LOW = 21
SOLL_TEMP_LOWLOW = 19
SOLL_BODENFEUCHTE_HIGH = 70
SOLL_BODENFEUCHTE_LOW = 50
SOLL_HELLIGKEIT_HIGH = 100
SOLL_HELLIGKEIT_LOW = 50
SOLL_LUFTFEUCHTE_HIGH = 95
SOLL_LUFTFEUCHTE_LOW = 80

#Sekundentakt
Takt = 1

#Zeitsteuerung Bewässerung
Start1 = 1
Start2 = 16
Start3 = 31
Start4 = 46

#Zeitsteuerung Beleuchtung
LampeAN = 7
LampeAUS = 19

class MCP3008:
def __init__(self, bus = 0, device = 0):
self.bus, self.device = bus, device
self.spi = SpiDev()
self.spi.open(self.bus, self.device)
# Dies ist noetig, weil ab Kernel 4.9 die Frequenz des SPI von 100 auf 125 MHz geaendert wurde (OST: 20180102)
self.spi.max_speed_hz = 1000000
def read(self, channel = 7):
adc = self.spi.xfer2([1, (8+channel)<<4, 0])
data = ((adc[1] & 3) << 8 ) + adc[2]
return data
def ConvertVolts(self, data, places):
volts = ((data) - float(1023)) * (-0.249266862) - 10
volts = round(volts,places)
return volts
def main ():
#try:
while True:
sleep(Takt)

#RTC lesen
Stunde = (int((time.strftime("%H"))))
Minute = (int((time.strftime("%M"))))
Sekunde = (int((time.strftime("%S"))))
#if Sekunde == 1:
print(time.strftime("%d.%m.%Y %H:%M:%S"))

#Helligkeit
lightLevel=readLight()
#print("Light Level : " + format(lightLevel,'.2f') + " lx")
#if Sekunde == 1:
print("LightLevel:", lightLevel)

#Analogsignale Feuchte
adc = MCP3008()
Feuchte1 = adc.ConvertVolts(int(adc.read(channel=7)),1)
Feuchte2 = adc.ConvertVolts(int(adc.read(channel=6)),1)
Feuchte3 = adc.ConvertVolts(int(adc.read(channel=4)),1)
Feuchte_Mittelwert = round((Feuchte1+Feuchte2+Feuchte3)/3, 1)
#if Sekunde ==1:
print (Feuchte1, Feuchte2, Feuchte3, Feuchte_Mittelwert)

#if Sekunde == 1:
#humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
#if humidity is not None and temperature is not None:
#print("TEST", 'Temp={0:0.1f}* Humidity={1:0.1f}%'.format(temperature, humidity))

if Sekunde == 10 or Sekunde == 30 or Sekunde == 50:
humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
if humidity is not None and temperature is not None:
print('Temp={0:0.1f}* Humidity={1:0.1f}%'.format(temperature, humidity))

if temperature > SOLL_TEMP_HIGH or humidity > SOLL_LUFTFEUCHTE_HIGH:
GPIO.output(Ventilator,True)
#print ('Ventilator an')

if temperature < SOLL_TEMP_LOW and humidity > SOLL_LUFTFEUCHTE_LOW:
GPIO.output(Ventilator,False)
print ('Ventilator aus')

if temperature < SOLL_TEMP_LOWLOW:
GPIO.output(Heizung,True)
#print ('Heizung an')

if temperature > SOLL_TEMP_LOW:
GPIO.output(Heizung,False)
print ('Heizung aus')

if Feuchte_Mittelwert >= SOLL_BODENFEUCHTE_HIGH:
#print('Anforderung Pumpe aus')
GPIO.output(Pumpe,False)

if Feuchte_Mittelwert <= SOLL_BODENFEUCHTE_LOW:
#print('Anforderung Pumpe an')

if Minute == Start1 and Sekunde == 1:
GPIO.output(Pumpe,True)
print("Wassergabe1",Minute, Sekunde)

if Minute == Start1 and Sekunde == 5:
GPIO.output(Pumpe,False)
print("Wassergabe1 fertig",Minute, Sekunde)

#if Minute == Start2 and Sekunde == 1:
#GPIO.output(Pumpe,True)
#print("Wassergabe1",Minute, Sekunde)

#if Minute == Start2 and Sekunde == 5:
#GPIO.output(Pumpe,False)
#print("Wassergabe1 fertig",Minute, Sekunde)

if Minute == Start3 and Sekunde == 1:
GPIO.output(Pumpe,True)
print("Wassergabe1",Minute, Sekunde)

if Minute == Start3 and Sekunde == 5:
GPIO.output(Pumpe,False)
print("Wassergabe1 fertig",Minute, Sekunde)

#if Minute == Start4 and Sekunde == 1:
#GPIO.output(Pumpe,True)
#print("Wassergabe1",Minute, Sekunde)

#if Minute == Start4 and Sekunde == 5:
#GPIO.output(Pumpe,False)
#print("Wassergabe1 fertig",Minute, Sekunde)

if Stunde == LampeAN and Sekunde == 1:
GPIO.output(Lampe,True)
print ('Lampe an')

if Stunde == LampeAUS and Sekunde == 1:
GPIO.output(Lampe,False)
print ('Lampe aus')

if __name__=="__main__":
main()

#except KeyboardInterrupt:
#print (" ENDE")

#GPIO.cleanup()
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Crossover: Das sieht alles ziemlich unübersichtlich und inkonsistent aus. Einrückung sollte vier Leerzeichen pro Ebene sein.

`sys` wird importiert, aber nicht verwendet.

Namen schreibt man in Python klein_mit_unterstrichen. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

Um das ``=`` bei Zuweisungen ausserhalb von Argumentlisten, um binäre Operatoren, und nach Kommas gehört ein Leerzeichen. Zwischen Funktion und öffnender Klammer eines Aufrufs dagegen gehört *kein* Leerzeichen.

Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Da steht einiges auf Modulebene, und das auch noch zwischen Funktions- und Klassendefinitionen verstreut, also maximal unübersichtlich, was mindestens in die Hauptfunktion gehört. Wobei die recht lang ist, und wahrscheinlich sinnvoll auf kleinere Funktionen aufgeteilt werden kann/sollte.

Konstantendefinitionen stehen in aller Regel vor Funktions- und Klassendefinitionen, nicht dazwischen.

Alles was Funktionen und Methoden ausser Konstanten benötigen wird als Argument(e) übergeben. Das betrifft `readLight()` das einfach so magisch auf `bus` zugreift. Das sollte aber nicht auf Modulebene existieren.

Das auskommentierte `GPIO.cleanup()` sollte man verwenden, und zwar so das es *immer* aufgerufen wird, nicht nur wenn der Benutzer Strg+C gedrückt hat. Also in einem `finally()`-Block. Dafür sollte man Warnungen nicht unterdrücken, sondern deren Ursache beheben. In der Regel ist das genau dieser fehlende `cleanup()`-Aufruf. Das `SMBus`-Exemplar ist auch etwas das ordnungsgemäss geschlossen werden sollte.

Für eine globale Konstante ist `pin` nicht nur falsch geschrieben, weil klein, sondern auch extrem allgemein benannt. Namen sollen dem Leser vermitteln was die Werte dahinter bedeuten. Das es eine Pin-Nummer ist, ist zwar nicht unwichtig, aber der Leser will doch wissen was an diesen Pin angeschlossen ist.

`DEVICE` ist zwar in Grossbuchstaben, also formal richtig, aber das sich dahinter die Adresse des über I²C angeschlossenen Helligkeitssensors verbirgt, sollte man auch am Namen schon ablesen können. Der Sinn Konstanten am Anfang zu definieren liegt darin das man nicht erst im Code danach suchen muss. Wenn die Konstante so generisch benannt ist, das man erst den Code suchen muss in dem sie verwendet wird, um zu verstehen was der Wert bedeutet, läuft dieser Idee zuwider.

`POWER_DOWN`, `POWER_ON`, und `RESET` werden nirgends verwendet‽

Nummerierte Namen sind immer ein Alarmzeichen. Man will sich da entweder passendere Namen ausdenken, oder gar keine einzelnen Namen/Werte verwenden, sondern eine Datenstruktur. Oft ist das eine Liste. So auch im Fall von `Start1` bis `Start4`. Der Code dafür ist vier mal exakt der gleiche, bloss eben für andere `Start*`-Werte. Das wäre also eine Liste mit den Werten und eine Schleife die über die Werte geht und wo der Code dann nur *einmal* drin steht, statt viermal. Code und Daten sollte man nicht wiederholen.

Die Art wie Stunde, Minute, und Sekunde ermittelt wird ist fehlerhaft. Du hast für jedes einen separaten Aufruf, zwischen den Aufrufen vergeht aber Zeit, so das die Werte nicht zum gleichen Zeitpunkt ermittelt werden, und damit nicht zusammenpassen müssen. Wenn Du die Stunde um 10:59:59 ermittelst, und die Minute dann aber schon um 11:00:00 abgefragt wird, liegt die Zeit von der das Programm ausgeht um fast eine ganze Stunde in der Vergangenheit. Zusätzlich ist das ermitteln und formatieren der Zeit als Zeichenkette nur um die dann wieder in eine Zahl umzuwandeln übermässig kompliziert. Wenn man dafür das `time`-Modul verwendet, dann die `localtime()`-Funktion, die *alle* Komponenten liefert, als *Zahlen*. Wobei das `time`-Modul eine hässliche API hat. Das ist einfach nur eine dünne Schicht über die Aufrufe in der C-Bibliothek. Eine ordentliche API bietet das `datetime`-Modul.

Bei der `MCP3008`-Klasse ist laut Traceback das Problem für den `OSError`. Da wird ein `SPIDev`-Objekt erstellt und beim `open()` kommt es dann zum Problem das zu viele Dateien offen sind. Das liegt daran das von der `MCP3008` jede Sekunde ein neues Objekt erstellt und darin dann diese `open()`-Methode aufgerufen wird, aber nirgends im Programm werden diese Dateien dann auch wieder geschlossen! Die `MCP3008`-Klasse braucht also eine `close()`-Methode die das an das `SPIDev`-Exemplar weiter reicht, und am besten macht man aus der Klasse auch gleich einen Kontextmanager wie das `SPIDev` schon ist, damit man ``with`` verwenden kann.

Die Attribute `bus` und `device` werden nirgends verwendet, das müssen also keine Attribute sein.

Die `ConvertVolts()`-”Methode” ist keine Methode sondern eine Funktion die in einer Klasse steckt. Das sollte man begründen können und mit `staticmethod()` klar machen, dass das Absicht ist. Wobei ich da eher eine `read_volts()`-Methode draus machen würde die man statt der `read()`-Methode aufruft.

Inhaltlich ist da ein unnötiges `float()` drin und zwei unnötige Klammerpaare die einfach nur *einen* Wert Klammern. Das `round()` hat da auch nichts drin zu suchen. `round()` braucht man nur sehr selten, nämlich dann, wenn man tatsächlich mit einem gerundeten Wert weiterrechnen will. Das ist hier nicht der Fall. Nachkommastellen für eine Ausgabe oder Umwandlung in eine Zeichenkette erledigt man dort per Formatierung.

Bei den Feuchtewerten sind wieder nummerierte Namen wo man stattdessen besser eine Liste verwendet.

Eine Bedingung nach dem Muster ``if a == x or a == y or a == z:`` lässt sich besser als ``if a in [x, y, z]:`` ausdrücken.

Kennst Du ``elif``? Da sehe ich hier Einsatzmöglichkeiten für.

Ermitteln von Werten bzw. Definition von Variablen sollte möglichst Nah an deren Verwendung passieren. Das macht den Code übersichtlicher und man kann auch leichter Teile einer Funktion in eine eigene Funktion herausziehen.

Etwas aufgeräumterer Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
# Gewächshaus2
from contextlib import closing
from datetime import datetime as DateTime
from statistics import mean
from time import sleep

import Adafruit_DHT
import smbus
from RPi import GPIO
from spidev import SpiDev

DHT_SENSOR_PIN = 12
LIGHT_SENSOR_I2C_ADDRESS = 0x23
CONTINUOUS_HIGH_RES_MODE_1 = 0x10

VENTILATOR_PIN = 5
HEIZUNG_PIN = 6
PUMPE_PIN = 13
LAMPE_PIN = 19

# Grenzwerte
SOLL_TEMP_HIGH = 23
SOLL_TEMP_LOW = 21
SOLL_TEMP_LOWLOW = 19
SOLL_BODENFEUCHTE_HIGH = 70
SOLL_BODENFEUCHTE_LOW = 50
SOLL_HELLIGKEIT_HIGH = 100
SOLL_HELLIGKEIT_LOW = 50
SOLL_LUFTFEUCHTE_HIGH = 95
SOLL_LUFTFEUCHTE_LOW = 80

TAKT = 1  # in Sekunden.

IRRIGATION_START_MINUTES = [1, 16, 31, 46]

STUNDE_LAMPE_AN = 7
STUNDE_LAMPE_AUS = 19


def read_light_level(bus, addr=LIGHT_SENSOR_I2C_ADDRESS):
    data = bus.read_i2c_block_data(addr, CONTINUOUS_HIGH_RES_MODE_1)
    return (data[1] + (256 * data[0])) / 1.2


class MCP3008:
    def __init__(self, bus=0, device=0):
        self.spi = SpiDev(bus, device)
        #
        # Dies ist nötig, weil ab Kernel 4.9 die Frequenz des SPI von 100 auf
        # 125 MHz geändert wurde (OST: 20180102)
        #
        self.spi.max_speed_hz = 1_000_000

    def __enter__(self):
        return self

    def __exit__(self, _exc_type, _exc_value, _traceback):
        self.close()

    def close(self):
        self.spi.close()

    def read(self, channel):
        data = self.spi.xfer2([1, (8 + channel) << 4, 0])
        return ((data[1] & 3) << 8) + data[2]

    def read_volts(self, channel):
        return (self.read(channel) - 1023) * -0.249266862 - 10


def main():
    with closing(smbus.SMBus(1)) as bus:
        try:
            GPIO.setmode(GPIO.BCM)
            GPIO.setup(
                [VENTILATOR_PIN, HEIZUNG_PIN, PUMPE_PIN, LAMPE_PIN], GPIO.OUT
            )
            while True:
                sleep(TAKT)

                now = DateTime.now()
                print(now)

                light_level = read_light_level(bus)
                print("Light level:", light_level)

                if now.second in [10, 30, 50]:
                    humidity, temperature = Adafruit_DHT.read_retry(
                        Adafruit_DHT.DHT22, DHT_SENSOR_PIN
                    )
                    if humidity is not None and temperature is not None:
                        print(
                            "Temp={0:0.1f}*  Humidity={1:0.1f}%".format(
                                temperature, humidity
                            )
                        )
                        if (
                            temperature > SOLL_TEMP_HIGH
                            or humidity > SOLL_LUFTFEUCHTE_HIGH
                        ):
                            GPIO.output(VENTILATOR_PIN, True)
                            # print ("Ventilator an")

                        if (
                            temperature < SOLL_TEMP_LOW
                            and humidity > SOLL_LUFTFEUCHTE_LOW
                        ):
                            GPIO.output(VENTILATOR_PIN, False)
                            print("Ventilator aus")

                        if temperature < SOLL_TEMP_LOWLOW:
                            GPIO.output(HEIZUNG_PIN, True)
                            # print ("Heizung an")
                        elif temperature > SOLL_TEMP_LOW:
                            GPIO.output(HEIZUNG_PIN, False)
                            print("Heizung aus")

                with MCP3008() as adc:
                    bodenfeuchte_werte = [
                        adc.read_volts(channel) for channel in [7, 6, 4]
                    ]
                bodenfeuchte_mittelwert = mean(bodenfeuchte_werte)
                print(*bodenfeuchte_werte, bodenfeuchte_mittelwert)

                if bodenfeuchte_mittelwert >= SOLL_BODENFEUCHTE_HIGH:
                    # print('Anforderung Pumpe aus')
                    GPIO.output(PUMPE_PIN, False)

                if bodenfeuchte_mittelwert <= SOLL_BODENFEUCHTE_LOW:
                    # print('Anforderung Pumpe an')
                    for start_minute in IRRIGATION_START_MINUTES:
                        if now.minute == start_minute:
                            if now.second == 1:
                                GPIO.output(PUMPE_PIN, True)
                                print("Wassergabe1", now.minute, now.second)
                            elif now.second == 5:
                                GPIO.output(PUMPE_PIN, False)
                                print(
                                    "Wassergabe1 fertig",
                                    now.minute,
                                    now.second,
                                )

                if now.second == 1:
                    if now.hour == STUNDE_LAMPE_AN:
                        GPIO.output(LAMPE_PIN, True)
                        print('Lampe an')
                    elif now.hour == STUNDE_LAMPE_AUS:
                        GPIO.output(LAMPE_PIN, False)
                        print('Lampe aus')
        finally:
            GPIO.cleanup()


if __name__ == "__main__":
    main()
Wie gesagt, die Hauptfunktion ist mir zu lang, die würde ich sinnvoll aufteilen.

Ganz Grundsätzlich ist aber auch das vorgehen mit den Zeiten nicht wirklich robust. Der Takt muss kleiner gewählt werden, denn `sleep()` kann auch etwas kürzer oder länger ”schlafen” + die Zeit die bei der Ausführung des Codes der da noch in der Schleife steht, kann locker mal dafür sorgen das so viel Zeit vergeht, dass `now.second` nicht nicht um eine sondern zwei Sekunden von einem zum nächsten Schleifendurchlauf ändert, so dass die Sekundengenauen vergleiche nicht treffen und beispielsweise die Pumpe nicht an, oder nicht wieder ausgeht. Gleiches mit der Lampe.

Wie schon gesagt würde ich das sowieso nicht selbst programmieren, es gibt dafür Packages wie APScheduler.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

@_blackjack_
Zunächst einmal vielen Dank für deine sehr ausführliche Kritik. Ich muss gestehen, dass ich mir das ganze Projekt zusammen kopiert und und zurecht gebogen habe. In mir reift die Erkenntnis, dass ich Python nicht wirklich verstanden habe und noch nicht einmal an der Oberfläche kratze. Der Zeitfaktor spielt da leider auch eine Rolle.

Aber... ich lasse mich nicht entmutigen. Meine grow box soll im Februar laufen, ansonsten habe ich alle Zeit der Welt etwas dazu zu lernen.
Mein Plan ist, erst erstmal zu verstehen, wie man strukturiert und was du an meinem script optimiert hast. Da ich alleine mit Google und dem Rheinwerk-Kompendium anscheinend nicht klar komme, muss ich mir wohl einige Grundlagen beibringen lassen. Klappt das deiner Meinung nach hier im Forum oder gibt es einen preiswerten und empfehlenswerten Kurs?
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das ist schwer zu sagen. In der Python-Dokumentation ist ein Tutorial, das man IMHO mal durchgearbeitet haben sollte um einen guten Überblick über die Sprache zu bekommen. Dabei kann es sein, dass es Sinn macht vorher ein anderes Tutorial/Buch/Kurs falls einem das Tutorial in der Python-Dokumentation zu schnell durch die Sprache geht.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

Hallo allerseits,
ich bin mit meinem Projekt etwas weiter gekommen und beschäftige mich gerade mit Funktionen und der Verwendung von Modulen. EIne Ulraschall-Füllstandmessung meines Wasserbehälters musste ich auslagern, da sie das timing in meinem Hauptscript beeinflusst hatte. Zudem hatte ich Probleme mit Fehlmessungen, die dann die Ausführung meines scripts verhindert haben. Für die Fehlerbehandlung habe ich nun eine Lösung.
@_blackjack_: Vielen Dank nochmal für deine Hilfestellung. So langsam kapiere ich Python. Habe auch schon ein wenig optimiert. Mit dem APScheduler beschäftige ich mich, wenn das erst einmal so funktioniert und ich mich an die weitere Optimierung begeben kann.

Ärger bereitet allerdings die Sensorik. Den Adafruit DHT Sensor (alle Varianten getestet) habe ich raus geworfen, weil die Hardware irgendwann beim Dauerbetrieb trotz Schaltung mit Widerstand versagt.
Ich versuche nun einen SI7021 über den i2c Bus auszulesen. Ich habe mir viele Beispiele angesehen, klappt nicht! Ich habe das Ganze mit Hilfe von Funktionen aufgebaut, immer der selbe Fehler, I/O Error! Das haben andere User auch berichtet. Ich habe alle Lösungsvorschläge in Python ausprobiert. Nichts hilft.
i2cdetect zeigt den Sensor auf Adresse 40 an, obwohl ich beim Anschluss kurz SDA und VIN vertauscht hatte. Peinliche Panne wegen Farbsehschwäche. Ich hoffe, das ist nicht der Grund.
Gibt es da eine Lösung, die mir entgangen ist? Kann mir jemand weiter helfen?

Fehlermeldung:
2019-12-23 20:15:14.835094
Nächster
Traceback (most recent call last):
File "/home/pi/Gewächshaus/humtemptest.py", line 38, in <module>
main()
File "/home/pi/Gewächshaus/humtemptest.py", line 29, in main
humidity = read_humidity(bus)
File "/home/pi/Gewächshaus/humtemptest.py", line 15, in read_humidity
data = bus.read_i2c_block_data(addr, SI7021_HUMIDITY_ADDRESS, 2)
OSError: [Errno 5] Input/output error

Mein script:

#!/usr/bin/env python3
# humtemptest
import smbus
import time
from contextlib import closing
from datetime import datetime as DateTime
from time import sleep

SI7021_I2C_ADDRESS = 0x40
SI7021_HUMIDITY_ADDRESS = 0xE5 #Startadresse, 2 bytes lesen
SI7021_TEMPERATURE_ADDRESS = 0xE3 #Startadresse, 2 bytes lesen
bus = smbus.SMBus(1)
time.sleep(1) #wait here to avoid 121 IO Error
def read_humidity(bus, addr=SI7021_I2C_ADDRESS):
data = bus.read_i2c_block_data(addr, SI7021_HUMIDITY_ADDRESS, 2)
return ((rh[0] * 256 + rh[1]) * 125 / 65536.0) - 6

def read_temperature(bus, addr=SI7021_I2C_ADDRESS):
data = bus.read_i2c_block_data(addr, SI7021_TEMPERATURE_ADDRESS, 2)
return ((temp[0] * 256 + temp[1]) * 175.72 / 65536.0) - 46.85

def main():
with closing(smbus.SMBus(1)) as bus:
try:
while True:
now = DateTime.now()
print(now)

humidity = read_humidity(bus)
temperature = read_temperature(bus)
print("Humidity / Temperature:", humidity, "/", temperature)
time.sleep(1)
finally:
print("Nächster")


if __name__ == "__main__":
main()
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Crossover: `sleep` wird aus `time` importiert aber nicht verwendet.

Du erstellst *zwei* `SMBus`-Objekte — ist das vielleicht schon das/ein Problem? Auf Modulebene hat das sowieso nichts zu suchen. Lass das dort doch einfach mal weg und verschiebe das Warten wegen dem anderen `IOError` in die Hauptfunktion.

Die beiden `read_*()`-Funktionen machen nichts mit `data` dafür versuchen beide in der Zeile danach auf Namen zuzugreifen die nicht definiert sind. Beide machen auch fast das gleiche — nur die Startadresse, ein Faktor, und ein Versatzwert unterscheiden sich jeweils bei den beiden Lesevorgängen. Das könnte man in eine gemeinsame Funktion heraus ziehen.

Code: Alles auswählen

#!/usr/bin/env python3
# humtemptest
import time
from contextlib import closing
from datetime import datetime as DateTime
from functools import partial

import smbus

SI7021_I2C_ADDRESS = 0x40
SI7021_HUMIDITY_ADDRESS = 0xE5  # Startadresse, 2 bytes lesen.
SI7021_TEMPERATURE_ADDRESS = 0xE3  # Startadresse, 2 bytes lesen.


def read_value(bus, i2c_address, start_address, factor, offset):
    data = bus.read_i2c_block_data(i2c_address, start_address, 2)
    return ((data[0] * 256 + data[1]) * factor / 65536) + offset


read_humidity = partial(
    read_value, SI7021_I2C_ADDRESS, SI7021_HUMIDITY_ADDRESS, 125, -6
)
read_temperature = partial(
    read_value, SI7021_I2C_ADDRESS, SI7021_TEMPERATURE_ADDRESS, 175.72, -46.85
)


def main():
    with closing(smbus.SMBus(1)) as bus:
        time.sleep(1)  # wait here to avoid 121 IO Error.
        try:
            while True:
                print(DateTime.now())
                humidity = read_humidity(bus)
                temperature = read_temperature(bus)
                print("Humidity / Temperature:", humidity, "/", temperature)
                time.sleep(1)
        finally:
            print("Nächster")


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

@_blackjack_: Ich glaube zu verstehen was du meinst. Mit read_value werden alle erforderlichen Informationen definiert damit anschließend die Ausgabe der beiden Messwerte gelesen und ermittelt werde kann. Das ist effizient und nachvollziehbar.
Mir fehlt noch Routine bei der Planung und leider auch noch einiges an Grundverständnis. Das sind halt die Nebenwirkungen, wenn man sich als Anfänger an die etwas anspruchsvolleren Themen wie Modularisierung und Bus-Kommunikation wagt.

Die SMBus Bibliothek ist etwas schwer zu durchschauen. Ich habe auch einige Beiträge und weniger gute Kritiken dazu gefunden.
Gibt es vielleicht aus diesem Grund eine SMBus2? Dazu finde ich umfangreiche Informationen. Ist diese Bibliothek vielleicht die bessere Wahl?

Nun zu meiner aktuellen Herausforderung bei der Definition read_value.
data = bus.read_i2c_block_data(i2c_address, start_address, 2) sollte, wenn ich das richtig verstanden habe, eine Variable "data" vom Datentyp Tuple sein? Warum meldet der Interpreter einen Attributfehler und wie könnte eine Lösung aussehen?

>>> %Run test_si7021.py
2019-12-28 11:23:42.457606
Nächster
Traceback (most recent call last):
File "/home/pi/Gewächshaus/test_si7021.py", line 37, in <module>
main()
File "/home/pi/Gewächshaus/test_si7021.py", line 28, in main
humidity = read_humidity(bus)
File "/home/pi/Gewächshaus/test_si7021.py", line 14, in read_value
data = bus.read_i2c_block_data(i2c_address, start_address, 2)
AttributeError: 'int' object has no attribute 'read_i2c_block_data'
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Die Variable “bus” ist nicht, was du glaubst was sie ist. Pack mal ein print(“der Bus:”, bus) davor.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Upsi, Fehler in meinem Programm: `bus` muss das letzte Argument von `read_value()` sein, nicht das erste. Das muss ja das einzige sein was nicht durch `partial()` an einen Wert gebunden wird.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

@_deets_: Ok, bus ist = 64 und anscheinend Integer. Code ich vorweg bus = smbus.SMBus(1) kommt es aufs gleiche heraus. Mit dieser Information und einer Dokumentation wie z. B. : http://wiki.erazor-zone.de/wiki:linux:p ... on_summary komme ich aber nicht so richtig weiter. Ist 64 = 6 bit oder 64 bit? Hilft es mir, die Fehlermeldung zu beseitigen?
Ich habe auch alle anderen Informationen von read_value ausgedruckt (i2c_address, start_address, factor, offset). Mit Ausnahme von offset scheinbar auch Integer. Mit der Hex-Adresse im offset kann ich erstmal auch nichts anfangen.

Ergebnis von print -> BUS: 64 229 125 -6 <SMBus object at 0x7697fbd8>

Ich raffe das einfach nicht. Im Netz finde ich nichts brauchbares, für mich verstandliches und im dicken Rheinberg-Wälzer zum Raspberry Pi findet man auf einer halben Seite zum Thema Python und i2c, wie ein Taster eine LED schaltet und erhält den oben genannten Verweis auf die wiki.erazor-zone Seite.
Hast du vielleicht bessere Informationsquellen oder eine hilfreiche Erklärung für einen ahnungslosen Anfänger wie mich?

Sorry, wenn ich nerve. Ich möchte an dieser Stelle einfach nicht aufgeben obwohl ich, mit Blick auf das Ergebnis für mein Projekt, zumindest eine zuverlässige Temperaturmessung, halt ohne Feuchtemessung hin zu bekommen, noch einen Notfallplan B habe.

@_deets_ & @_blackjack_: Vielen Dank für eure Geduld und Unterstützung. Ohne euch wäre ich lange nicht soweit mit meiner growbox. Die Kiste läuft, mit Ausnahme der Temperatursteuerung bisher Störungsfrei im Dauertestbetrieb. Ich weiß nun auch, dass man für die LED-Pflanzenbeleichtung besser einen Einschaltstrombegrenzer haben sollte, zumindest wenn man die gängigen China-Relaisplatinen verwendet.
Crossover
User
Beiträge: 11
Registriert: Donnerstag 10. Oktober 2019, 18:50

@_blackjack_: Ich hatte deine Nachricht noch nicht gelesen, als ich _deets_ schrieb.
Habe gerade voller Hoffnung auf eine Lösung des Problems, das Argument 'bus' nach hinten sortiert.

def read_value(i2c_address, start_address, factor, offset, bus):
print("args read_value:", i2c_address, start_address, factor, offset, bus)
data = bus.read_i2c_block_data(i2c_address, start_address, 2)
return ((data[0] * 256 + data[1]) * factor / 65536) + offset

Nun kommt, wie in meinen vorherigen Versuchen wieder der I/O Fehler 5.

>>> %Run test_si7021.py
2019-12-28 18:33:52.196558
args read_value: 64 229 125 -6 <SMBus object at 0x768aa9e0>
Nächster
Traceback (most recent call last):
File "/home/pi/Gewächshaus/test_si7021.py", line 38, in <module>
main()
File "/home/pi/Gewächshaus/test_si7021.py", line 29, in main
humidity = read_humidity(bus)
File "/home/pi/Gewächshaus/test_si7021.py", line 15, in read_value
data = bus.read_i2c_block_data(i2c_address, start_address, 2)
OSError: [Errno 5] Input/output error

Wie stellst du deinen code im Forumsbeitrag eigentlich strukturiert dar?
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das sind die Code Tags, im vollständigen Editor der </> knopf.

Und mir klingt das so, als ob du ein defekten Sensor oder nicht richtig aufgebauten Bus hast. Sind da Pull-ups dran? Hast du andere I2C Sensoren die du mal testen kannst?
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Crossover: Der Fehler kann eine Menge Ursachen haben. Nix angeschlossen, diverse Hardwareprobleme, falsche Baudrate, …

Für Code gibt es hier Code-Tags. Im vollständigen Editor die Schaltfläche mit der Aufschrift </> oder eben manuell eingegeben.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten