FrĂŒher waren Heimautomatisierungssysteme, oder wie sie oft genannt werden âintelligentes Hausâ, extrem teuer und nur fĂŒr Wohlhabende erschwinglich. Heute findet man auf dem Markt recht preiswerte Sets mit Sensoren, Tasten/Schaltern und Aktoren zur Steuerung von Beleuchtung, Steckdosen, BelĂŒftung, Wasserversorgung und anderen Verbrauchern. Selbst der ungeschickteste DIY-Fan kann sich dem Wunderbaren anschlieĂen und gĂŒnstig GerĂ€te fĂŒr ein intelligentes Zuhause zusammenstellen.

In der Regel sind die angebotenen GerĂ€te entweder Sensoren oder Aktoren. Sie ermöglichen es, Szenarien wie âbei Bewegungserkennung das Licht einschaltenâ oder âder Schalter am Ausgang schaltet das Licht in der gesamten Wohnung ausâ einfach zu realisieren. Aber mit der Telemetrie hat es nicht so gut geklappt. Im besten Fall gibt es ein Diagramm der Temperatur und Luftfeuchtigkeit oder die momentane Leistung an einer bestimmten Steckdose.
KĂŒrzlich habe ich mir WasserzĂ€hler mit Impulsausgang installiert. Nach jedem Liter, der durch den ZĂ€hler flieĂt, wird ein Reed-Schalter betĂ€tigt und schlieĂt den Kontakt. Es bleibt nur noch, die Kabel anzuschlieĂen und zu versuchen, daraus Nutzen zu ziehen. Zum Beispiel den Wasserverbrauch nach Stunden und Wochentagen zu analysieren. Wenn es in der Wohnung mehrere Wasserleitungen gibt, ist es praktischer, alle aktuellen Werte auf einem Bildschirm zu sehen, als mit einer Taschenlampe in schwer zugĂ€ngliche Nischen zu kriechen.
Unter dem Cut findet ihr meine Version eines GerĂ€ts auf Basis des ESP8266, das die Impulse von den WasserzĂ€hlern zĂ€hlt und die Daten ĂŒber MQTT an den Smart Home Server sendet. Wir werden in micropython programmieren und die Bibliothek uasyncio verwenden. Bei der Erstellung der Firmware bin ich auf einige interessante Schwierigkeiten gestoĂen, ĂŒber die ich in diesem Artikel ebenfalls berichten werde. Los geht's!
Diagramm

Das Herz der gesamten Schaltung ist der Modul mit Mikrocontroller ESP8266. UrsprĂŒnglich war der ESP-12 geplant, aber meiner war fehlerhaft. Ich musste mich mit dem ESP-07 zufrieden geben, der vorrĂ€tig war. GlĂŒcklicherweise sind sie sowohl in den AnschlĂŒssen als auch in der FunktionalitĂ€t identisch, der einzige Unterschied ist in der Antenne â beim ESP-12 ist sie eingebaut, beim ESP-07 ist sie extern. Allerdings wird selbst ohne Antenne das WiFi-Signal in meinem Badezimmer gut empfangen.
Die Verdrahtung des Moduls ist Standard:
- Reset-Knopf mit Pull-Up und Kondensator (obwohl beides bereits im Modul vorhanden ist)
- Das Enable-Signal (CH_PD) ist an die Stromversorgung angeschlossen.
- GPIO15 ist mit Masse verbunden. Das ist nur beim Start notwendig, aber ich habe sowieso nichts, was ich an diesen Pin anschlieĂen könnte.
Um das Modul in den Programmiermodus zu versetzen, muss GPIO2 mit Masse verbunden werden. Zur Erleichterung habe ich einen Boot-Knopf vorgesehen. Im normalen Zustand wird dieser Pin mit Strom versorgt.
Der Status der GPIO2-Leitung wird nur zu Beginn der Arbeit ĂŒberprĂŒft â bei der Einspeisung von Strom oder sofort nach dem Reset. Das Modul lĂ€dt entweder wie gewohnt oder wechselt in den Programmiermodus. Nach dem Laden kann dieser Pin als normaler GPIO verwendet werden. Da bereits ein Knopf vorhanden ist, kann ich ihm auch eine nĂŒtzliche Funktion zuweisen.
FĂŒr Programmierung und Debugging werde ich UART verwenden, das ich auf das Breakout-Board herausgefĂŒhrt habe. Bei Bedarf schlieĂe ich einfach einen USB-UART-Adapter an. Ich darf nur nicht vergessen, dass das Modul mit 3,3V betrieben wird. Wenn ich vergesse, den Adapter auf diese Spannung umzustellen und 5V anlege, brennt das Modul wahrscheinlich durch.
Mit Strom in der Dusche habe ich keine Probleme â die Steckdose befindet sich etwa einen Meter von den ZĂ€hlern entfernt, also werde ich es mit 220V betreiben. Als Stromquelle wird ein kleiner von Tenstar Robot fungieren. Ich selbst habe Schwierigkeiten mit analoger und Leistungselektronik, und hier ist ein fertiges Netzteil in einem kleinen GehĂ€use.
Zur Anzeige der Betriebsmodi habe ich eine LED vorgesehen, die an GPIO2 angeschlossen ist. Ich habe sie jedoch nicht verlötet, da im ESP-07-Modul bereits eine LED vorhanden ist, die ebenfalls an GPIO2 angeschlossen ist. Aber auf der Platine darf sie sein â vielleicht möchte ich diese LED ja mal nach auĂen fĂŒhren.
Kommen wir zu dem Interessantesten. Die WasserzĂ€hler haben keine Logik, man kann sie nicht nach den aktuellen Messwerten fragen. Das Einzige, was verfĂŒgbar ist, sind Impulse â das SchlieĂen der Kontakte des Reed-Relais pro Liter. Die AusgĂ€nge der Reed-Relais sind auf GPIO12/GPIO13 gefĂŒhrt. Den Pull-Up-Widerstand werde ich programmatisch innerhalb des Moduls aktivieren.
UrsprĂŒnglich hatte ich die WiderstĂ€nde R8 und R9 nicht eingeplant und in meiner Platine fehlen sie. Aber da ich das Schaltbild nun zur allgemeinen Einsicht bereitstelle, sollte ich diesen Fehler beheben. Die WiderstĂ€nde sind notwendig, um den Port nicht zu beschĂ€digen, falls die Firmware fehlerhaft ist und eine Eins an dem Pin ausgibt, wĂ€hrend das Reed-Relais diese Verbindung mit der Masse kurzschlieĂt (mit dem Widerstand flieĂen maximal 3,3V/1000Ω = 3,3mA).
Es ist an der Zeit zu ĂŒberlegen, was zu tun ist, wenn der Strom ausfĂ€llt. Die erste Option besteht darin, vom Server zu Beginn die Ausgangswerte der ZĂ€hler anzufordern. Das wĂŒrde jedoch zu einer erheblichen KomplexitĂ€t des Austauschprotokolls fĂŒhren. DarĂŒber hinaus wĂ€re die FunktionsfĂ€higkeit des GerĂ€ts in diesem Fall vom Zustand des Servers abhĂ€ngig. Wenn der Server nach dem Stromausfall nicht wieder gestartet werden kann (oder spĂ€ter gestartet wird), könnte der WasserzĂ€hler die Ausgangswerte nicht anfordern und wĂŒrde nicht korrekt arbeiten.
Deshalb habe ich beschlossen, die Werte der ZĂ€hler in einem Speicherchip zu speichern, der ĂŒber I2C angeschlossen ist. Es gibt keine speziellen Anforderungen an die GröĂe des Flash-Speichers â ich muss nur 2 Zahlen speichern (die Menge an Litern aus den ZĂ€hlern fĂŒr heiĂes und kaltes Wasser). Selbst das kleinste Modul wĂŒrde ausreichen. Achten muss man jedoch auf die Anzahl der Schreibzyklen. Bei den meisten Modulen betrĂ€gt diese 100.000 Zyklen, bei einigen sogar bis zu einer Million.
Auf den ersten Blick mag eine Million viel erscheinen. Aber in den vier Jahren, in denen ich in meiner Wohnung gelebt habe, habe ich etwas mehr als 500 Kubikmeter Wasser verbraucht, das sind 500.000 Liter! Und 500.000 Aufzeichnungen im Flash. Und das sind nur kaltes Wasser. Man könnte natĂŒrlich den Chip alle paar Jahre neu löten, aber es gibt auch FRAM-Chips. In Bezug auf die Programmierung ist das dasselbe wie I2C EEPROM, nur mit einer unglaublichen Anzahl von Wiederbeschreibungszyklen (Hunderte Millionen). Nur komme ich einfach nicht dazu, ein GeschĂ€ft mit solchen Chips zu besuchen, deshalb bleibt vorerst der herkömmliche 24LC512.
Leiterplatte
UrsprĂŒnglich hatte ich geplant, die Platine in Heimarbeit herzustellen. Daher wurde die Platine als einseitig konzipiert. Aber nachdem ich ĂŒber eine Stunde mit dem LaserlötgerĂ€t und der Lötmaske gekĂ€mpft habe (ohne sie geht es irgendwie nicht), habe ich mich entschieden, die Platinen bei den Chinesen zu bestellen.

Kurz bevor ich die Platine bestellen wollte, fiel mir ein, dass man zusĂ€tzlich zum Flash-Speicherchip noch etwas NĂŒtzliches an den I2C-Bus anschlieĂen könnte, zum Beispiel ein Display. Was genau darauf angezeigt werden soll, ist noch eine Frage, aber man muss es auf der Platine verlegen. Und da ich die Platinen in der Fabrik bestellen wollte, machte es keinen Sinn mehr, mich auf eine einseitige Platine zu beschrĂ€nken, deshalb sind die Linien fĂŒr I2C die einzigen auf der RĂŒckseite der Platine.
Ein groĂes Missgeschick war mit dem einseitigen Layout verbunden. Da die Platine einseitig gezeichnet wurde, war geplant, die Leiterbahnen und SMD-Bauteile auf einer Seite anzuordnen und abgehende Bauteile, AnschlĂŒsse und das Netzteil auf der anderen Seite. Als ich nach einem Monat die Platinen erhielt, hatte ich den ursprĂŒnglichen Plan vergessen und alle Komponenten auf der Vorderseite montiert. Erst als es darum ging, das Netzteil anzulöten, stellte ich fest, dass Plus und Minus vertauscht waren. Ich musste improvisieren und mit BrĂŒcken arbeiten. Auf dem Bild oben habe ich das Layout bereits geĂ€ndert, aber die Masse wird von einem Teil der Platine zum anderen ĂŒber die AnschlĂŒsse des Boot-Buttons gefĂŒhrt (obwohl man auch auf der zweiten Schicht eine Leiterbahn hĂ€tte verlegen können).
So sieht es aus

GehÀuse
Der nĂ€chste Schritt ist das GehĂ€use. Mit einem 3D-Drucker ist das kein Problem. Ich habe mich nicht viel damit beschĂ€ftigt â einfach eine Box in der benötigten GröĂe entworfen und die Ausschnitte an den richtigen Stellen gemacht. Der Deckel wird mit kleinen Schrauben am GehĂ€use befestigt.

Ich habe bereits erwĂ€hnt, dass der Boot-Button als allgemeine Taste verwendet werden kann â also werden wir ihn auf die Vorderseite bringen. DafĂŒr habe ich einen speziellen "Schacht" entworfen, in dem der Button untergebracht ist.

Innert des GehÀuses befinden sich auch Noppen, auf denen die Platine montiert wird und mit einer einzigen M3-Schraube fixiert wird (auf der Platine war nicht mehr Platz).
Das Display wĂ€hlte ich, nachdem ich den ersten Prototyp des GehĂ€uses gedruckt hatte. Ein Standard-Doppelfeld passte nicht in dieses GehĂ€use, aber im Vorrat fand ich ein OLED-Display SSD1306 128Ă32. Es ist klein, aber ich schaue nicht jeden Tag darauf â es reicht.
Ich dachte darĂŒber nach, wie die Kabel verlegt werden und entschied mich, das Display in der Mitte des GehĂ€uses anzubringen. Die Ergonomie ist natĂŒrlich miserabel â der Button oben, das Display unten. Aber ich habe bereits gesagt, dass die Idee, das Display anzubringen, zu spĂ€t kam und ich keine Lust hatte, die Platine neu zu layouten, um den Button zu verschieben.
Das GerĂ€t ist zusammengebaut. Das Displaymodul ist mit HeiĂkleber befestigt.


Das Endergebnis kann auf KDVP gesehen werden.
Firmware
Kommen wir zum Softwareteil. FĂŒr solche kleinen Projekte arbeite ich sehr gerne mit der Programmiersprache Python (). Der Code ist sehr kompakt und verstĂ€ndlich. Zum GlĂŒck ist es hier nicht notwendig, auf der Registerebene zu agieren, um Mikrosekunden herauszuholen â alles kann in Python erledigt werden.
Es scheint einfach zu sein, ist es aber nicht â im GerĂ€t sind mehrere unabhĂ€ngige Funktionen vorgesehen:
- Der Benutzer drĂŒckt einen Knopf und schaut auf das Display.
- Die Liter zÀhlen und aktualisieren die Werte im Flash-Speicher.
- Das Modul ĂŒberwacht das WiFi-Signal und reconnectet sich bei Bedarf.
- Ohne die blinkende Lampe geht es ĂŒberhaupt nicht.
Es darf nicht passieren, dass eine Funktion nicht lĂ€uft, wĂ€hrend eine andere aus irgendeinem Grund hĂ€ngt. Ich habe in anderen Projekten genug auf die Nase bekommen und sehe jetzt schon die Bugs im Stil von "wir haben einen weiteren Liter verpasst, weil in diesem Moment das Display aktualisiert wurde" oder "der Benutzer kann nichts tun, wĂ€hrend das Modul sich mit WiFi verbindet". NatĂŒrlich können einige Dinge durch Interrupts erledigt werden, aber man kann auf EinschrĂ€nkungen bezĂŒglich Dauer, Verschachtelung von Aufrufen oder nicht-atomaren Ănderungen von Variablen stoĂen. Und der Code, der alles gleichzeitig macht, verwandelt sich schnell in ein Chaos.
Im Ich habe klassische preemptive Multitasking und FreeRTOS verwendet, aber in diesem Fall war das Modell Die Python-Implementierung der Koroutinen ist einfach bombastisch â alles ist einfach und bequem fĂŒr den Programmierer gemacht. Man muss nur die Logik schreiben und angeben, wo zwischen den Threads gewechselt werden kann.
Die Unterschiede zwischen preemptive und kooperativem Multitasking empfehle ich optional zu studieren. Und jetzt lasst uns endlich zum Code ĂŒbergehen.
#####################################
# Counter class - implements a single water counter on specified pin
#####################################
class Counter():
debounce_ms = const(25)
def __init__(self, pin_num, value_storage):
self._value_storage = value_storage
self._value = self._value_storage.read()
self._value_changed = False
self._pin = Pin(pin_num, Pin.IN, Pin.PULL_UP)
loop = asyncio.get_event_loop()
loop.create_task(self._switchcheck()) # Thread runs foreverJeder ZĂ€hler wird von einer Instanz der Klasse Counter verarbeitet. ZunĂ€chst wird der Startwert des ZĂ€hlers aus EEPROM (value_storage) ausgelesen â so erfolgt die Wiederherstellung nach einem Stromausfall.
Der Pin wird mit internem Pull-Up zur Stromversorgung initialisiert: Wenn der Reed-Kontakt geschlossen ist, betrÀgt der Zustand auf der Leitung null; wenn er geöffnet ist, wird die Leitung zur Stromversorgung gezogen, und der Controller liest eins.
AuĂerdem wird hier ein separater Task gestartet, der das Pin abfragen wird. Jeder ZĂ€hler wird seinen eigenen Task starten. Hier ist dessen Code:
""" Pin abfragen und den Wert erhöhen, wenn ein weiterer Liter durchlaufen ist """
async def _switchcheck(self):
last_checked_pin_state = self._pin.value() # Anfangszustand abfragen
# Auf eine PinÀnderung abfragen
while True:
state = self._pin.value()
if state != last_checked_pin_state:
# Zustand hat sich geÀndert: Jetzt handeln.
last_checked_pin_state = state
if state == 0:
self._another_litre_passed()
# Weitere ZustandsÀnderungen ignorieren, bis der Schalter sich stabilisiert hat
await asyncio.sleep_ms(Counter.debounce_ms)Eine Verzögerung von 25 ms ist notwendig, um das Rauschen von Kontakten zu filtern, und gleichzeitig reguliert sie, wie oft die Aufgabe aufwacht (wĂ€hrend diese Aufgabe schlĂ€ft, arbeiten andere Aufgaben). Alle 25 ms wacht die Funktion auf, ĂŒberprĂŒft den Pin und wenn die Kontakte des Reed-Relais geschlossen sind, bedeutet das, dass ein weiterer Liter durch den ZĂ€hler geflossen ist und dies bearbeitet werden muss.
def _another_litre_passed(self):
self._value += 1
self._value_changed = True
self._value_storage.write(self._value)Die Bearbeitung des nĂ€chsten Liters ist trivial â der ZĂ€hler wird einfach erhöht. AuĂerdem wĂ€re es gut, den neuen Wert auf den Flash-Speicher zu schreiben.
Zur besseren Nutzung gibt es "Zugriffsobjekte".
def value(self):
self._value_changed = False
return self._value
def set_value(self, value):
self._value = value
self._value_changed = FalseUnd jetzt nutzen wir die VorzĂŒge von Python und der Bibliothek uasync und machen das ZĂ€hler-Objekt wartbar (wie ĂŒbersetzt man das ins Russische? Das, auf das man warten kann?)
def __await__(self):
while not self._value_changed:
yield from asyncio.sleep(0)
return self.value()
__iter__ = __await__ Das ist eine nĂŒtzliche Funktion, die wartet, bis der ZĂ€hlerwert aktualisiert wird â die Funktion wacht gelegentlich auf und ĂŒberprĂŒft das Flag _value_changed. Der Clou an dieser Funktion ist, dass der aufrufende Code bei diesem Funktionsaufruf schlafen kann und bis zur Bereitstellung eines neuen Wertes schlafen kann.
Und was ist mit Interrupts?Ja, hier könnt ihr mich gerne trollen, dass ich selbst von Interrupts gesprochen habe, aber in Wirklichkeit einen dummen Pin-Abfrage eingerichtet habe. TatsĂ€chlich war Interrupts das Erste, was ich ausprobierte. Im ESP8266 kann man einen Interrupt am Frontende einrichten und sogar einen Interrupt-Handler in Python schreiben. In diesem Interrupt kann der Wert einer Variable aktualisiert werden. Wahrscheinlich wĂŒrde das genĂŒgen, wenn der ZĂ€hler als Slave-GerĂ€t fungiert â also ein GerĂ€t, das wartet, bis jemand nach diesem Wert fragt.
Leider (oder vielleicht zum GlĂŒck?) ist mein GerĂ€t aktiv, es sollte selbststĂ€ndig Nachrichten ĂŒber das MQTT-Protokoll senden und Daten im EEPROM aufzeichnen. Hier gibt es bereits EinschrĂ€nkungen â im Interrupt kann man keinen Speicher zuweisen und keinen groĂen Stack verwenden, also kann man das Senden von Nachrichten ĂŒber das Netzwerk vergessen. Es gibt nĂŒtzliche Funktionen wie micropython.schedule(), die es ermöglichen, eine Funktion âsofort wie möglichâ auszufĂŒhren, aber dann stellt sich die Frage: âWas bringt das?â Plötzlich senden wir gerade irgendeine Nachricht, und da kommt ein Interrupt dazwischen, der die Werte der Variablen durcheinanderbringt. Oder zum Beispiel, ein neues ZĂ€hlerwert ist vom Server angekommen, wĂ€hrend wir das alte noch nicht geschrieben haben. Insgesamt muss man eine Synchronisation aufbauen oder sich anders behelfen.
AuĂerdem tritt von Zeit zu Zeit ein RuntimeError: schedule stack full auf, und wer weiĂ, warum?
Mit expliziter Abfrage und uasync ist es in diesem Fall irgendwie schöner und zuverlÀssiger.
Die Arbeit mit dem EEPROM habe ich in eine kleine Klasse ausgelagert.
class EEPROM():
i2c_addr = const(80)
def __init__(self, i2c):
self.i2c = i2c
self.i2c_buf = bytearray(4) # Vermeidet das Erstellen/Zerstören des Puffers bei jedem Aufruf
def read(self, eeprom_addr):
self.i2c.readfrom_mem_into(self.i2c_addr, eeprom_addr, self.i2c_buf, addrsize=16)
return ustruct.unpack_from("<I", self.i2c_buf)[0]
def write(self, eeprom_addr, value):
ustruct.pack_into("<I", self.i2c_buf, 0, value)
self.i2c.writeto_mem(self.i2c_addr, eeprom_addr, self.i2c_buf, addrsize=16)In Python ist es schwierig, direkt mit Bytes zu arbeiten, und genau die Bytes werden im Speicher geschrieben. Daher musste ich eine Konvertierung zwischen Ganzzahlen und Bytes unter Verwendung der Bibliothek ustruct erstellen.
Damit ich nicht jedes Mal das I2C-Objekt und die Speicheradresse ĂŒbergeben muss, habe ich das Ganze in eine kleine und praktische Klasse gepackt.
class EEPROMValue():
def __init__(self, i2c, eeprom_addr):
self._eeprom = EEPROM(i2c)
self._eeprom_addr = eeprom_addr
def read(self):
return self._eeprom.read(self._eeprom_addr)
def write(self, value):
self._eeprom.write(self._eeprom_addr, value)Das I2C-Objekt wird mit diesen Parametern erstellt.
i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4))Jetzt kommen wir zu dem spannendsten Teil â der Implementierung der Kommunikation mit dem Server ĂŒber MQTT. Das Protokoll selbst muss nicht implementiert werden â in den Weiten des Internets fand sich ein . Die werden wir verwenden.
All das Interessante ist in der Klasse CounterMQTTClient gesammelt, die auf der Bibliotheksklasse MQTTClient basiert. Lassen Sie uns mit der Peripherie beginnen,
#####################################
# Class handles both counters and sends their status to MQTT
#####################################
class CounterMQTTClient(MQTTClient):
blue_led = Pin(2, Pin.OUT, value = 1)
button = Pin(0, Pin.IN)
hot_counter = Counter(12, EEPROMValue(i2c, EEPROM_ADDR_HOT_VALUE))
cold_counter = Counter(13, EEPROMValue(i2c, EEPROM_ADDR_COLD_VALUE))Hier werden die Pins fĂŒr die Lampe und die Taste eingerichtet und konfiguriert sowie die Objekte fĂŒr kaltes und warmes Wasser erstellt.
Mit der Initialisierung ist es nicht so trivial.
def __init__(self):
self.internet_outage = True
self.internet_outages = 0
self.internet_outage_start = ticks_ms()
with open("config.txt") as config_file:
config['ssid'] = config_file.readline().rstrip()
config['wifi_pw'] = config_file.readline().rstrip()
config['server'] = config_file.readline().rstrip()
config['client_id'] = config_file.readline().rstrip()
self._mqtt_cold_water_theme = config_file.readline().rstrip()
self._mqtt_hot_water_theme = config_file.readline().rstrip()
self._mqtt_debug_water_theme = config_file.readline().rstrip()
config['subs_cb'] = self.mqtt_msg_handler
config['wifi_coro'] = self.wifi_connection_handler
config['connect_coro'] = self.mqtt_connection_handler
config['clean'] = False
config['clean_init'] = False
super().__init__(config)
loop = asyncio.get_event_loop()
loop.create_task(self._heartbeat())
loop.create_task(self._counter_coro(self.cold_counter, self._mqtt_cold_water_theme))
loop.create_task(self._counter_coro(self.hot_counter, self._mqtt_hot_water_theme))
loop.create_task(self._display_coro())FĂŒr die Konfiguration der Funktionsparameter der Bibliothek mqtt_as wird ein groĂ angelegtes Wörterbuch verschiedener Einstellungenâ configâverwendet. Der GroĂteil der Voreinstellungen passt uns, aber viele Einstellungen mĂŒssen ausdrĂŒcklich festgelegt werden. Um die Einstellungen nicht direkt im Code anzugeben, speichere ich sie in einer Textdatei config.txt. Dies ermöglicht es, den Code unabhĂ€ngig von den Einstellungen zu Ă€ndern und mehrere identische GerĂ€te mit unterschiedlichen Parametern zu produzieren.
Der letzte Block des Codes startet mehrere Coroutinen zur Wartung verschiedener Systemfunktionen. Hier ist beispielsweise die Coroutine, die die ZĂ€hler betreut.
async def _counter_coro(self, counter, topic):
# Anfangswert veröffentlichen
value = counter.value()
await self.publish(topic, str(value))
# Jedes neue Wert veröffentlichen
while True:
value = await counter
await self.publish_msg(topic, str(value))Die Coroutine wartet in einer Schleife auf den neuen Wert des ZĂ€hlers und sendet, sobald dieser verfĂŒgbar ist, eine Nachricht ĂŒber das MQTT-Protokoll. Der erste StĂŒck Code sendet den Anfangswert, auch wenn kein Wasser durch den ZĂ€hler flieĂt.
Die Basisklasse MQTTClient verwaltet sich selbst, initiiert selbst die Verbindung ĂŒber WiFi und stellt wieder eine Verbindung her, wenn die Verbindung abbricht. Bei Ănderungen des Verbindungsstatus informiert uns die WiFi-Bibliothek durch einen Aufruf von wifi_connection_handler.
async def wifi_connection_handler(self, state):
self.internet_outage = not state
if state:
self.dprint('WiFi ist aktiv.');
duration = ticks_diff(ticks_ms(), self.internet_outage_start) // 1000
await self.publish_debug_msg('WiederverbundenNach', duration)
else:
self.internet_outages += 1
self.internet_outage_start = ticks_ms()
self.dprint('WiFi ist nicht aktiv.');
await asyncio.sleep(0)Die Funktion ist ehrlich aus Beispielen ĂŒbernommen. In diesem Fall zĂ€hlt sie die Anzahl der AusfĂ€lle (internet_outages) und deren Dauer. Bei der Wiederherstellung der Verbindung wird die Ausfallzeit an den Server gesendet.
Ăbrigens ist der letzte Sleep nur notwendig, damit die Funktion asynchron wird - in der Bibliothek wird sie ĂŒber await aufgerufen, ansonsten können nur Funktionen aufgerufen werden, die ein anderes await enthalten.
Neben der Verbindung zum WiFi muss auch eine Verbindung zum MQTT-Broker (Server) hergestellt werden. Diese Aufgabe ĂŒbernimmt ebenfalls die Bibliothek, wĂ€hrend wir die Möglichkeit haben, etwas NĂŒtzliches zu tun, sobald die Verbindung hergestellt ist.
async def mqtt_connection_handler(self, client):
await client.subscribe(self._mqtt_cold_water_theme)
await client.subscribe(self._mqtt_hot_water_theme)Hier abonnieren wir mehrere Nachrichten - der Server hat jetzt die Möglichkeit, die aktuellen ZÀhlerstÀnde zu setzen, indem er die entsprechende Nachricht sendet.
def mqtt_msg_handler(self, topic, msg):
topicstr = str(topic, 'utf8')
self.dprint("Empfangene MQTT-Nachricht topic={}, msg={}".format(topicstr, msg))
if topicstr == self._mqtt_cold_water_theme:
self.cold_counter.set_value(int(msg))
if topicstr == self._mqtt_hot_water_theme:
self.hot_counter.set_value(int(msg))Diese Funktion verarbeitet die empfangenen Nachrichten, und je nach Thema (Titel der Nachricht) werden die Werte eines der ZĂ€hler aktualisiert.
Ein paar Hilfsfunktionen
# Publish a message if WiFi and broker is up, else discard
async def publish_msg(self, topic, msg):
self.dprint("Publishing message on topic {}: {}".format(topic, msg))
if not self.internet_outage:
await self.publish(topic, msg)
else:
self.dprint("Message was not published - no internet connection")Diese Funktion sendet eine Nachricht, wenn die Verbindung hergestellt ist. Wenn keine Verbindung besteht, wird die Nachricht ignoriert.
Und das ist einfach eine nĂŒtzliche Funktion, die Debugging-Nachrichten erstellt und sendet.
async def publish_debug_msg(self, subtopic, msg):
await self.publish_msg("{}\/{}".format(self._mqtt_debug_water_theme, subtopic), str(msg))
So viel Text, und wir haben noch nicht die LED zum Blinken gebracht. Hier ist es.
# Blink flash LED if WiFi down
async def _heartbeat(self):
while True:
if self.internet_outage:
self.blue_led(not self.blue_led()) # Fast blinking if no connection
await asyncio.sleep_ms(200)
else:
self.blue_led(0) # Rare blinking when connected
await asyncio.sleep_ms(50)
self.blue_led(1)
await asyncio.sleep_ms(5000)Ich habe 2 Blinkmodi vorgesehen. Wenn die Verbindung verloren geht (oder sie gerade hergestellt wird), blinkt das GerÀt schnell. Wenn die Verbindung hergestellt ist, blinkt das GerÀt einmal alle 5 Sekunden. Bei Bedarf können hier auch andere Blinkmodi implementiert werden.
Aber die LED ist nur ein Gimmick. Wir haben uns ja auch dem Display zugewandt.
async def _display_coro(self):
display = SSD1306_I2C(128,32, i2c)
while True:
display.poweron()
display.fill(0)
display.text("COLD: {:.3f}".format(self.cold_counter.value() / 1000), 16, 4)
display.text("HOT: {:.3f}".format(self.hot_counter.value() / 1000), 16, 20)
display.show()
await asyncio.sleep(3)
display.poweroff()
while self.button():
await asyncio.sleep_ms(20)Das ist es, worĂŒber ich gesprochen habe â wie einfach und bequem es mit Koroutinen ist. Diese kleine Funktion beschreibt das gesamte Benutzerinteraktionsverhalten. Die Koroutine wartet einfach auf das DrĂŒcken eines Knopfes und schaltet das Display fĂŒr 3 Sekunden ein. Auf dem Display werden die aktuellen ZĂ€hlerstĂ€nde angezeigt.
Es sind noch ein paar Kleinigkeiten ĂŒbrig. Hier ist die Funktion, die all das (neu) startet. Die Hauptschleife beschĂ€ftigt sich lediglich damit, verschiedene Debug-Informationen einmal pro Minute zu senden. Insgesamt bringe ich es so, wie es ist â ich denke, es braucht keine besonderen Kommentare.
async def main(self):
while True:
try:
await self._connect_to_WiFi()
await self._run_main_loop()
except Exception as e:
self.dprint('Globale Kommunikationsstörung: ', e)
await asyncio.sleep(20)
async def _connect_to_WiFi(self):
self.dprint('Verbinde mit WiFi und MQTT')
sta_if = network.WLAN(network.STA_IF)
sta_if.connect(config['ssid'], config['wifi_pw'])
conn = False
while not conn:
await self.connect()
conn = True
self.dprint('Verbunden!')
self.internet_outage = False
async def _run_main_loop(self):
# Schleife fĂŒr immer
mins = 0
while True:
gc.collect() # FĂŒr RAM-Statistiken.
mem_free = gc.mem_free()
mem_alloc = gc.mem_alloc()
try:
await self.publish_debug_msg("Betriebszeit", mins)
await self.publish_debug_msg("Neuveröffentlichungen", self.REPUB_COUNT)
await self.publish_debug_msg("Störungen", self.internet_outages)
await self.publish_debug_msg("SpeicherFrei", mem_free)
await self.publish_debug_msg("SpeicherBelegt", mem_alloc)
except Exception as e:
self.dprint("Ausnahme aufgetreten: ", e)
mins += 1
await asyncio.sleep(60)Nun noch ein paar Einstellungen und Konstanten zur VollstÀndigkeit der Beschreibung.
#####################################
# Constants and configuration
#####################################
config['keepalive'] = 60
config['clean'] = False
config['will'] = ('/ESP/Wemos/Water/LastWill', 'Goodbye cruel world!', False, 0)
MQTTClient.DEBUG = True
EEPROM_ADDR_HOT_VALUE = const(0)
EEPROM_ADDR_COLD_VALUE = const(4)So wird das Ganze gestartet.
client = CounterMQTTClient()
loop = asyncio.get_event_loop()
loop.run_until_complete(client.main())Irgendetwas stimmt nicht mit meinem Speicher.
So, der gesamte Code ist da. Ich habe die Dateien mit dem Tool ampy hochgeladen â es ermöglicht, sie auf den internen (den, der im ESP-07 ist) Flash-Speicher hochzuladen und sie dann aus dem Programm wie gewöhnliche Dateien zuzugreifen. Auch die von mir verwendeten Bibliotheken mqtt_as, uasyncio, ssd1306 und collections (die innerhalb von mqtt_as verwendet werden) habe ich dort hochgeladen.
Wir starten und⊠bekommen einen MemoryError. Je mehr ich versuchte zu verstehen, wo genau der Speicherverlust auftritt, desto schneller trat dieser Fehler auf, je mehr Debug-Ausgaben ich einfĂŒgte. Eine kurze Google-Suche brachte mich zu der Erkenntnis, dass der Mikrocontroller insgesamt nur 30 kB Speicher hat, in die 65 kB Code (einschlieĂlich der Bibliotheken) einfach nicht passen.
Aber es gibt einen Ausweg. Es stellt sich heraus, dass Micropython den Code nicht direkt aus der .py-Datei ausfĂŒhrt â diese Datei wird zuerst kompiliert. AuĂerdem wird sie direkt im Mikrocontroller kompiliert, wandelt sich in Bytecode, der dann im Speicher gespeichert wird. Und um den Compiler zu betreiben, wird auch eine bestimmte Menge an RAM benötigt.
Der Trick besteht darin, den Mikrocontroller von ressourcenintensiver Kompilierung zu befreien. Man kann die Dateien auf einem leistungsstarken Computer kompilieren und den bereits fertigen Bytecode auf den Mikrocontroller hochladen. Dazu muss man das Micropython-Firmware herunterladen und .
Ich habe keinen Makefile geschrieben, sondern bin manuell durchgegangen und habe alle erforderlichen Dateien (einschlieĂlich der Bibliotheken) ungefĂ€hr so kompiliert
mpy-cross water_counter.pyJetzt mĂŒssen nur noch die Dateien mit der Endung .mpy hochgeladen werden, wobei man nicht vergessen sollte, die entsprechenden .py-Dateien vorher aus dem Dateisystem des GerĂ€ts zu löschen.
Die gesamte Entwicklung habe ich in der Programmumgebung (IDE?) ESPlorer durchgefĂŒhrt. Sie ermöglicht es, Skripte auf den Mikrocontroller hochzuladen und sie sofort auszufĂŒhren. In meinem Fall befindet sich die gesamte Logik und die Erstellung aller Objekte in der Datei water_counter.py (.mpy). Um das alles jedoch beim Start automatisch auszufĂŒhren, muss es auch eine Datei mit dem Namen main.py geben. Dabei muss es sich um eine .py-Datei handeln, nicht um den vorab kompilierten .mpy. Hier ist ihr trivialisierbarer Inhalt
import water_counterWir starten â alles funktioniert. Aber es gibt bedrohlich wenig freien Speicher â etwa 1 kB. Ich habe auch PlĂ€ne zur Erweiterung der FunktionalitĂ€t des GerĂ€ts, und dieses Kilobyte wird mir eindeutig nicht ausreichen. Aber es stellte sich heraus, dass es auch dafĂŒr eine Lösung gibt.
Das Problem ist Folgendes. Selbst wenn die Dateien in Bytecode kompiliert sind und sich im internen Dateisystem befinden, werden sie dennoch in den RAM geladen und von dort ausgefĂŒhrt. Aber Micropython kann den Bytecode auch direkt aus dem Flash-Speicher ausfĂŒhren, dafĂŒr muss er jedoch direkt in die Firmware eingebettet werden. Das ist nicht schwierig, obwohl es auf meinem Netbook eine Weile dauerte (dort hatte ich Linux).
Der Algorithmus ist folgender:
- Herunterladen und installieren . Dieses Ding erstellt den Compiler und die Bibliotheken fĂŒr Programme fĂŒr den ESP8266. Es wird gemÀà den Anweisungen auf der Hauptseite des Projekts zusammengestellt (ich habe die Installation STANDALONE=yes gewĂ€hlt)
- Herunterladen
- Die erforderlichen Bibliotheken in ports/esp8266/modules innerhalb des Micropython-Baums ablegen
- Die Firmware gemÀà den Anweisungen in der Datei zu erstellen
- Wir laden die Firmware in den Mikrocontroller (ich mache das unter Windows mit den Programmen ESP8266Flasher oder dem Python-Tool esptool).
Jetzt wird âimport ssd1306â den Code direkt aus der Firmware aufrufen, und dafĂŒr wird kein RAM verbraucht. Mit diesem Trick habe ich nur den Code der Bibliotheken in die Firmware geladen, wĂ€hrend der Hauptcode meines Programms von dem Dateisystem ausgefĂŒhrt wird. Das ermöglicht eine leichte Modifikation des Programms, ohne die Firmware neu zu kompilieren. Momentan habe ich noch etwa 8,5 kB RAM frei. Das ermöglicht es, in Zukunft noch viele nĂŒtzliche Funktionen umzusetzen. Wenn der Speicher wirklich nicht ausreicht, kann ich auch das Hauptprogramm in die Firmware stopfen.
Und was soll ich jetzt damit machen?
Okay, die Hardware ist zusammengebaut, die Firmware ist geschrieben, das GehĂ€use ist gedruckt, das GerĂ€t ist an die Wand befestigt und blinkt fröhlich mit einer Lampe. Aber bisher ist das alles eine Black Box (im ĂŒbertragenen und wörtlichen Sinne) und der Nutzen ist noch gering. Es ist an der Zeit, etwas mit den MQTT-Nachrichten zu machen, die an den Server gesendet werden.
Mein "smarter Zuhause" lĂ€uft auf . Das MQTT-Modul ist entweder von Anfang an vorhanden oder lĂ€sst sich leicht aus dem Add-On-Markt installieren â ich erinnere mich nicht mehr, woher ich es bekommen habe. MQTT ist nicht autark â es benötigt einen sogenannten Broker â einen Server, der MQTT-Nachrichten empfangt, sortiert und an die Clients weiterleitet. Ich benutze mosquitto, das (wie auch majordomo) auf demselben Netbook lĂ€uft.
Nachdem das GerÀt einmal eine Nachricht gesendet hat, wird der Wert sofort in der Liste angezeigt.

Diese Werte können jetzt mit Objekten im System verknĂŒpft werden, sie können in Automatisierungsszenarien verwendet und verschiedenen Analysen unterzogen werden â das alles fĂ€llt nicht in den Rahmen dieses Artikels. Wer an dem Majordomo-System interessiert ist, kann ich empfehlen, â ein Kollege, der ebenfalls ein smartes Zuhause baut und verstĂ€ndlich ĂŒber die Konfiguration des Systems spricht.
Ich werde nur ein paar Diagramme zeigen. Das ist ein einfaches Diagramm der Werte ĂŒber 24 Stunden.

Es ist zu erkennen, dass nachts fast niemand Wasser genutzt hat. Ein paar Mal ist jemand auf die Toilette gegangen, und anscheinend saugt der Umkehrosmosefilter in der Nacht ein paar Liter. Am Morgen steigt der Verbrauch erheblich an. Normalerweise benutze ich Wasser aus dem Boiler, aber hier wollte ich ein Bad nehmen und habe vorĂŒbergehend auf das stĂ€dtische Warmwasser umgeschaltet â das ist ebenfalls gut im unteren Diagramm zu sehen.
Aus diesem Diagramm habe ich erfahren, dass der Gang zur Toilette etwa 6-7 l Wasser kostet, eine Dusche 20-30 l, GeschirrspĂŒlen etwa 20 l erfordert und ein Vollbad 160 l benötigt. An einem Tag verbraucht meine Familie etwa 500-600 l.
FĂŒr besonders Neugierige können die Aufzeichnungen zu jedem einzelnen Wert eingesehen werden.

Hier habe ich herausgefunden, dass das Wasser bei geöffnetem Wasserhahn mit einer Geschwindigkeit von ungefĂ€hr 1 l in 5 Sekunden flieĂt.
Aber in dieser Form ist es wahrscheinlich nicht sehr bequem, die Statistiken anzusehen. In majordomo gibt es auch die Möglichkeit, die Verbrauchsdiagramme nach Tagen, Wochen und Monaten anzusehen. Hier, zum Beispiel, ein SÀulendiagramm des Verbrauchs.

Im Moment habe ich nur Daten fĂŒr eine Woche. In einem Monat wird dieses Diagramm anschaulicher sein â jedem Tag wird eine separate SĂ€ule entsprechen. Ein wenig stört das Bild die manuelle Anpassung der Werte, die ich eingebe (die gröĂte SĂ€ule). Und es ist noch unklar, ob ich die allerersten Werte fast um einen Kubik weniger falsch eingegeben habe oder ob es ein Fehler in der Firmware ist und nicht alle Liter angerechnet wurden. Ich brauche mehr Zeit.
An den Diagrammen selbst muss noch gefeilt, gestrichen und lackiert werden. Möglicherweise werde ich auch zu Testzwecken ein Diagramm ĂŒber den Speicherverbrauch erstellen â vielleicht gibt es da ein Leck. Vielleicht werde ich auch die ZeitrĂ€ume angeben, in denen kein Internet vorhanden war. Bis jetzt schwirrt das alles nur auf der Ebene einer Idee.
Fazit
Heute ist meine Wohnung ein kleines StĂŒck schlauer geworden. Mit diesem kleinen GerĂ€t wird es fĂŒr mich einfacher sein, den Wasserverbrauch im Haus zu ĂŒberwachen. Wenn ich frĂŒher oft dachte: "Wieder viel Wasser im Monat verbraucht", kann ich jetzt die Quelle dieses Verbrauchs ausfindig machen.
FĂŒr manche mag es seltsam erscheinen, die Anzeigen auf dem Bildschirm zu sehen, wenn er einen Meter vom ZĂ€hler entfernt ist. Aber in naher Zukunft plane ich, in eine andere Wohnung umzuziehen, wo es mehrere Wasserleitungen geben wird, und die ZĂ€hler wahrscheinlich auf der Treppe stehen. Daher wird ein GerĂ€t zur Fernablesung der ZĂ€hlerstĂ€nde sehr nĂŒtzlich sein.
Die FunktionalitĂ€t des GerĂ€ts plane ich ebenfalls zu erweitern. Ich sehe mir bereits motorisierte Ventile an. Jetzt muss ich zur Umschaltung zwischen Boiler und Stadtwasser drei schwer zugĂ€ngliche Ventile drehen. Es wĂ€re viel bequemer, dies mit einem einzigen Knopfdruck und entsprechender Anzeige zu tun. Und natĂŒrlich sollte auch ein Schutz vor Leckagen implementiert werden.
In dem Artikel habe ich meine Variante eines GerĂ€ts auf Basis des ESP8266 vorgestellt. Meiner Meinung nach ist mir eine sehr interessante Firmware-Variante in micropython unter Verwendung von Koroutinen gelungen â einfach und ansprechend. Ich habe versucht, viele Feinheiten und Fehler zu beschreiben, auf die ich gestoĂen bin. Möglicherweise habe ich alles zu detailliert beschrieben, fĂŒr mich als Leser ist es einfacher, Unnötiges zu ĂŒberspringen, als spĂ€ter das zu ergĂ€nzen, was nicht gesagt wurde.
Wie immer bin ich offen fĂŒr konstruktive Kritik.
Quelle: habr.com
