Wir verbinden einen Wasserzähler mit einem Smart Home

Früher waren Hausautomationssysteme, oder wie sie oft als „Smart Home“ bezeichnet werden, furchtbar teuer und konnten sich nur reiche Leute leisten. Heutzutage gibt es auf dem Markt recht kostengünstige Kits mit Sensoren, Tasten / Schaltern und Aktuatoren zur Steuerung von Beleuchtung, Steckdosen, Belüftung, Wasserversorgung und anderen Verbrauchern. Und selbst der krivoruky DIY-Shnik kann sich den schönen anschließen und Geräte für ein Smart Home zu günstigen Preisen sammeln.



In der Regel sind die vorgeschlagenen Geräte entweder Sensoren oder Aktoren. Sie erleichtern die Implementierung von Szenarien wie „Schalten Sie das Licht ein, wenn der Bewegungssensor ausgelöst wird“ oder „Der Schalter am Ausgang löscht das Licht in der gesamten Wohnung“. Aber die Telemetrie hat irgendwie nicht geklappt. Im besten Fall ist dies ein Diagramm der Temperatur und Luftfeuchtigkeit oder der momentanen Leistung in einer bestimmten Steckdose.

Kürzlich habe ich einen Wasserzähler mit Impulsausgang installiert. Ein Reedschalter wird durch jeden Liter durch das Messgerät ausgelöst und schließt den Kontakt. Das einzige, was noch übrig bleibt, ist, sich an die Drähte zu klammern und zu versuchen, davon zu profitieren. Analysieren Sie beispielsweise den Wasserverbrauch nach Stunde und Wochentag. Wenn es in der Wohnung mehrere Steigleitungen für Wasser gibt, ist es bequemer, alle aktuellen Anzeigen auf einem Bildschirm zu sehen, als schwer zugängliche Nischen mit einer Taschenlampe zu besteigen.

Unter dem Schnitt basiert meine Version des Geräts auf dem ESP8266, der Impulse von Wasserzählern zählt und Messwerte über MQTT an den Smart-Home-Server sendet. Wir werden in Mikropython mit der Uasyncio-Bibliothek programmieren. Bei der Erstellung der Firmware bin ich auf einige interessante Schwierigkeiten gestoßen, die ich auch in diesem Artikel diskutieren werde. Lass uns gehen!

Schema




Das Herzstück der gesamten Schaltung ist das Modul des Mikrocontrollers ESP8266. ESP-12 war ursprünglich geplant, aber meins erwies sich als defekt. Ich musste mich mit dem verfügbaren ESP-07-Modul zufrieden geben. Glücklicherweise sind sie sowohl in Bezug auf die Schlussfolgerungen als auch in Bezug auf die Funktionalität gleich. Der Unterschied besteht nur in der Antenne - der ESP-12 verfügt über eine integrierte Antenne und der ESP-07 über eine externe Antenne. Aber auch ohne WiFi-Antenne wird das Signal in meinem Badezimmer normal empfangen.

Die Modulbindung ist Standard:

  • Reset-Taste mit Hosenträger und Kondensator (obwohl sich beide bereits im Modul befinden)
  • Aktivierungssignal (CH_PD) an die Stromversorgung gezogen
  • GPIO15 zu Boden gezogen. Dies ist nur zu Beginn notwendig, aber ich habe immer noch nichts mehr an diesem Bein festzuhalten

Um das Modul in den Firmware-Modus zu versetzen, müssen Sie den GPIO2 gegen Masse schließen, und um es bequemer zu machen, habe ich die Boot-Taste bereitgestellt. Im Normalzustand wird dieser Stift an die Stromversorgung gezogen.

Der Status der GPIO2-Leitung wird nur zu Beginn der Arbeit überprüft - beim Einschalten oder unmittelbar nach einem Reset. Das Modul wird also entweder wie gewohnt geladen oder wechselt in den Firmware-Modus. Nach dem Laden kann dieser Ausgang als normaler GPIO verwendet werden. Nun, da es dort bereits eine Schaltfläche gibt, können Sie eine nützliche Funktion darauf setzen.

Zum Programmieren und Debuggen werde ich den UART verwenden, der zum Kamm gebracht wurde. Bei Bedarf stecke ich einfach einen USB-UART-Adapter ein. Sie müssen sich nur daran erinnern, dass das Modul mit 3,3 V betrieben wird. Wenn Sie vergessen, den Adapter auf diese Spannung umzuschalten und 5 V anzulegen, brennt das Modul höchstwahrscheinlich aus.

Ich habe keine Probleme mit Strom im Badezimmer - die Steckdose befindet sich ungefähr einen Meter von den Zählern entfernt, also werde ich sie mit 220 V versorgen. Als Stromquelle werde ich einen kleinen Block HLK-PM03 von Tenstar Robot bearbeiten. Persönlich bin ich eng mit Analog- und Leistungselektronik verbunden, aber hier ist ein fertiges Netzteil in einem kleinen Fall.

Um die Betriebsarten zu signalisieren, habe ich eine an GPIO2 angeschlossene LED bereitgestellt. Ich habe jedoch nicht angefangen, es zu löten, weil Das ESP-07-Modul verfügt außerdem bereits über eine LED, die an denselben GPIO2 angeschlossen ist. Aber lass es auf der Platine sein - plötzlich möchte ich diese LED zum Gehäuse bringen.

Wir gehen zu den interessantesten über. Wasserzähler haben keine Logik und können nicht nach aktuellen Messwerten gefragt werden. Das einzige, was uns zur Verfügung steht, sind Impulse - das Schließen der Kontakte des Reed-Schalters pro Liter. Die Schlussfolgerungen der Reed-Schalter sind in GPIO12 / GPIO13 festgelegt. Ich werde den Pull-up-Widerstand programmgesteuert im Modul einschalten.

Anfangs habe ich vergessen, die Widerstände R8 und R9 bereitzustellen, und in meiner Version der Platine sind dies nicht der Fall. Da ich das Programm jedoch bereits öffentlich ausstelle, lohnt es sich, dieses Versehen zu korrigieren. Es werden Widerstände benötigt, um den Anschluss nicht zu verbrennen, wenn die Firmware fehlerhaft ist und das Gerät auf den Pin setzt, und der Reed-Schalter schließt diese Leitung gegen Masse kurz (maximal 3,3 V / 1000 Ohm = 3,3 mA fließen mit dem Widerstand).

Es ist Zeit zu überlegen, was zu tun ist, wenn der Strom ausfällt. Die erste Möglichkeit besteht darin, den Server beim Start nach ersten Zählern zu fragen. Dies würde jedoch eine erhebliche Komplikation des Austauschprotokolls erfordern. Darüber hinaus hängt die Funktionsfähigkeit des Geräts in diesem Fall vom Status des Servers ab. Wenn der Server nach dem Ausschalten des Lichts nicht gestartet wurde (oder später gestartet wurde), konnte der Wasserzähler die Anfangswerte nicht anfordern und funktionierte nicht ordnungsgemäß.

Daher habe ich mich entschlossen, die Speicherung von Zählerwerten in einem über I2C verbundenen Speicherchip zu implementieren. Ich habe keine besonderen Anforderungen an die Größe des Flash-Speichers - ich muss nur 2 Zahlen speichern (die Anzahl der Liter in Heiß- und Kaltwasserzählern). Selbst das kleinste Modul reicht aus. Bei der Anzahl der Aufnahmezyklen müssen Sie jedoch aufpassen. Für die meisten Module sind dies 100.000 Zyklen, für einige bis zu einer Million.

Es scheint, eine Million ist viel. Aber für 4 Jahre in meiner Wohnung habe ich etwas mehr als 500 Kubikmeter Wasser verbraucht, das sind 500.000 Liter! Und 500 Tausend Einträge im Blitz. Und das ist nur kaltes Wasser. Sie können den Chip natürlich alle paar Jahre löten, aber es stellte sich heraus, dass es FRAM-Chips gibt. Aus programmtechnischer Sicht ist dies das gleiche I2C-EEPROM, jedoch mit einer sehr großen Anzahl von Umschreibungszyklen (Hunderte von Millionen). Das ist nur so lange, bis alles mit solchen Chips in irgendeiner Weise in den Laden kommt, also wird der übliche 24LC512 vorerst bestehen bleiben.

Leiterplatte


Anfangs wollte ich zu Hause eine Gebühr erheben. Daher wurde das Board einseitig gestaltet. Aber nach einer langen Stunde mit einem Lasereisen und einer Lötmaske (ohne dass es irgendwie komisch wäre) entschied ich mich immer noch, Bretter bei den Chinesen zu bestellen.



Fast vor der Bestellung der Karte wurde mir klar, dass Sie neben dem Flash-Speicherchip am I2C-Bus noch etwas Nützliches wie ein Display mitnehmen können. Was genau darauf ausgegeben werden soll, ist noch eine Frage, aber Sie müssen auf dem Brett züchten. Nun, da ich die Boards im Werk bestellen wollte, machte es keinen Sinn, mich auf ein einseitiges Board zu beschränken, daher sind die Leitungen auf I2C die einzigen auf der Rückseite des Boards.

Ein großer Pfosten war auch mit einseitiger Verkabelung verbunden. Weil Die Platine wurde auf der einen Seite gezeichnet, die Schienen und SMD-Komponenten sollten auf der einen Seite und die Ausgangskomponenten, Anschlüsse und das Netzteil auf der anderen Seite platziert werden. Als ich die Boards in einem Monat erhielt, vergaß ich den ursprünglichen Plan und entpackte alle Komponenten auf der Vorderseite. Und nur beim Löten des Netzteils stellte sich heraus, dass Plus und Minus im Gegenteil geschieden waren. Ich musste mit Springern Kollektivfarmen betreiben. Im obigen Bild habe ich die Verkabelung bereits geändert, aber die Masse wird über die Ausgänge der Boot-Taste von einem Teil der Platine zum anderen geworfen (obwohl es möglich wäre, eine Spur auf der zweiten Ebene zu zeichnen).

Es stellte sich so heraus



Gehäuse


Der nächste Schritt ist das Gehäuse. Mit einem 3D-Drucker ist dies kein Problem. Ich habe mich nicht viel darum gekümmert - ich habe nur eine Schachtel mit der richtigen Größe gezeichnet und an den richtigen Stellen Ausschnitte gemacht. Die Abdeckung wird mit kleinen Schrauben am Gehäuse befestigt.



Ich habe bereits erwähnt, dass die Boot-Taste als Allzweck-Taste verwendet werden kann - hier bringen wir sie auf die Vorderseite. Zu diesem Zweck habe ich einen speziellen „Brunnen“ gezeichnet, in dem der Knopf lebt.



Im Inneren des Gehäuses befinden sich auch Stümpfe, auf denen die Platine mit einer einzigen M3-Schraube installiert und befestigt wird (auf der Platine war kein Platz mehr).

Das Display wurde ausgewählt, als ich die erste passende Version des Gehäuses druckte. Die Standard-Zweilinie passte nicht in dieses Gehäuse, aber im Gimbal wurde ein OLED-Display SSD1306 128x32 gefunden. Es ist klein, aber ich muss ihn nicht jeden Tag ansehen - es wird rollen.

Ich schätzte beide Richtungen und wie die Drähte von ihm verlegt werden, und beschloss, das Display in die Mitte des Gehäuses zu stecken. Ergonomie natürlich unter der Fußleiste - ein Knopf oben, ein Display unten. Aber ich habe bereits gesagt, dass die Idee, das Display zu vermasseln, zu spät kam und es zu faul war, das Board neu anzuordnen, um den Knopf zu bewegen.

Das Gerät ist fertig. Das Anzeigemodul wird auf die Schmelzdüsen geklebt





Das Endergebnis ist auf KDPV zu sehen

Firmware


Fahren wir mit dem Softwareteil fort. Für solch kleine Handwerke verwende ich sehr gerne die Python-Sprache ( Micropython ) - der Code ist sehr kompakt und verständlich. Glücklicherweise ist es nicht erforderlich, auf die Registerebene zu gehen, um Mikrosekunden zu komprimieren - alles kann von Python aus erfolgen.

Es scheint alles einfach, aber nicht sehr einfach zu sein - mehrere unabhängige Funktionen sind im Gerät beschrieben:

  • Der Benutzer drückt eine Taste und schaut auf das Display
  • Liter ticken und aktualisieren die Werte im Flash-Speicher
  • Das Modul überwacht das WLAN-Signal und stellt bei Bedarf eine neue Verbindung her
  • Nun, ohne ein blinkendes Licht können Sie es überhaupt nicht tun

Es ist unmöglich anzunehmen, dass eine Funktion nicht funktioniert hat, wenn die andere aus irgendeinem Grund dumm ist. Ich habe bereits in anderen Projekten Kakteen gegessen, und jetzt sehe ich Störungen im Stil „einen weiteren Liter verpasst, weil in diesem Moment die Anzeige aktualisiert wurde“ oder „der Benutzer kann nichts tun, während das Modul eine Verbindung zu WiFi herstellt“. Natürlich können einige Dinge durch Interrupts erledigt werden, aber Sie können auf eine Einschränkung der Dauer, der Verschachtelung von Aufrufen oder der nichtatomaren Änderung von Variablen stoßen. Nun, der Code, der alles macht und sich sofort schnell in ein Chaos verwandelt.

In dem Projekt habe ich das klassische präemptive Multitasking und FreeRTOS ernsthafter verwendet, aber in diesem Fall erwiesen sich das Coroutine- Modell und die Uasync-Bibliothek als viel geeigneter. Darüber hinaus ist die Pitonovskiy-Implementierung von Corutin nur eine Bombe - für den Programmierer war alles einfach und bequem erledigt. Schreiben Sie einfach Ihre eigene Logik und sagen Sie mir, an welchen Stellen Sie zwischen Threads wechseln können.

Ich schlage vor, die Unterschiede zwischen Verdrängung und wettbewerbsfähigem Multitasking optional zu untersuchen. Kommen wir nun endlich zum Code.

##################################### # 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 forever 

Jeder Zähler wird von einer Instanz der Counter-Klasse verarbeitet. Zunächst wird der Anfangswert des Zählers vom EEPROM (value_storage) abgezogen - auf diese Weise wird die Wiederherstellung nach einem Stromausfall implementiert.

Der Pin wird mit einem eingebauten Pull-up zur Stromversorgung initialisiert: Wenn der Reed-Schalter geschlossen ist, ist er Null in der Leitung. Wenn die Leitung offen ist, wird die Leitung an die Stromversorgung gezogen und der Controller liest eins.

Außerdem wird hier eine separate Aufgabe gestartet, die den Pin abfragt. Jeder Zähler führt seine eigene Aufgabe aus. Hier ist ihr Code

  """ Poll pin and advance value when another litre passed """ async def _switchcheck(self): last_checked_pin_state = self._pin.value() # Get initial state # Poll for a pin change while True: state = self._pin.value() if state != last_checked_pin_state: # State has changed: act on it now. last_checked_pin_state = state if state == 0: self._another_litre_passed() # Ignore further state changes until switch has settled await asyncio.sleep_ms(Counter.debounce_ms) 

Zum Filtern des Kontaktsprungs ist eine Verzögerung von 25 ms erforderlich, die gleichzeitig regelt, wie oft die Aufgabe aufwacht (während diese Aufgabe schläft, funktionieren andere Aufgaben). Alle 25 ms wacht die Funktion auf, überprüft den Stift und wenn die Kontakte des Reed-Schalters geschlossen sind, ist ein weiterer Liter durch den Zähler geflossen und dieser muss verarbeitet werden.

  def _another_litre_passed(self): self._value += 1 self._value_changed = True self._value_storage.write(self._value) 

Die Verarbeitung des nächsten Liters ist trivial - der Zähler erhöht sich einfach. Nun, ein neuer Wert wäre schön, auf ein Flash-Laufwerk zu schreiben.

Zur Vereinfachung der Verwendung werden „Accessoren“ bereitgestellt.

  def value(self): self._value_changed = False return self._value def set_value(self, value): self._value = value self._value_changed = False 

Nun nutzen wir die Freuden von Python und der Uasync-Bibliothek und machen das Gegenobjekt wartbar (wie kann dies ins Russische übersetzt werden? Das, was zu erwarten ist?)

  def __await__(self): while not self._value_changed: yield from asyncio.sleep(0) return self.value() __iter__ = __await__ 

Dies ist eine so praktische Funktion, die wartet, bis der Zählerwert aktualisiert wird. Die Funktion wird von Zeit zu Zeit aktiviert und überprüft das Flag _value_changed. Der Witz dieser Funktion ist, dass der aufrufende Code bei einem Aufruf dieser Funktion einschlafen und schlafen kann, bis ein neuer Wert empfangen wird.

Aber was ist mit Interrupts?
Ja, an dieser Stelle können Sie mich trollen und sagen, dass er selbst über Unterbrechungen gesprochen hat, aber tatsächlich hat er eine dumme Umfrage über die Stecknadel arrangiert. In der Tat sind Interrupts das erste, was ich versucht habe. In ESP8266 können Sie einen Interrupt am Rand organisieren und sogar einen Handler für diesen Interrupt in Python schreiben. In diesem Interrupt können Sie den Wert einer Variablen aktualisieren. Wahrscheinlich würde dies ausreichen, wenn der Zähler ein Slave-Gerät wäre - eines, das wartet, bis es nach diesem Wert gefragt wird.

Leider (oder zum Glück?) Ist mein Gerät aktiv, es muss Nachrichten mit dem MQTT-Protokoll senden und Daten in das EEPROM schreiben. Und hier kommen die Einschränkungen bereits ins Spiel - Sie können keinen Speicher zuweisen und keinen großen Stapel in Interrupts verwenden, was bedeutet, dass Sie das Senden von Nachrichten über das Netzwerk vergessen können. Es gibt Brötchen wie micropython.schedule (), mit denen Sie eine Funktion "sofort" ausführen können, aber die Frage lautet "Was ist der Sinn?". Plötzlich senden wir gerade eine Art Nachricht, und hier verkeilt und unterbricht der Interrupt die Werte der Variablen. Oder es ist beispielsweise ein neuer Zählerwert vom Server eingetroffen, während wir den alten noch nicht notiert haben. Im Allgemeinen müssen Sie die Synchronisation umzäunen oder irgendwie anders raus.

Und von Zeit zu Zeit stürzt RuntimeError ab: Zeitplanstapel voll und wer weiß warum?

Mit einer expliziten Umfrage und Uasync ist es in diesem Fall irgendwie schöner und zuverlässiger.

Ich habe in einer kleinen Klasse mit EEPROM gearbeitet

 class EEPROM(): i2c_addr = const(80) def __init__(self, i2c): self.i2c = i2c self.i2c_buf = bytearray(4) # Avoid creation/destruction of the buffer on each call 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, aber es sind Bytes, die in den Speicher geschrieben werden. Ich musste die Konvertierung zwischen Integer und Bytes mithilfe der Ustruct-Bibliothek korrigieren.

Um das I2C-Objekt und die Speicherzellenadresse nicht jedes Mal zu übertragen, habe ich alles in einen kleinen und praktischen Klassiker verpackt

 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 selbst wird mit solchen Parametern erstellt

 i2c = I2C(freq=400000, scl=Pin(5), sda=Pin(4)) 

Wir nähern uns dem Interessantesten - der Implementierung der Kommunikation mit dem Server über MQTT. Nun, das Protokoll selbst muss nicht implementiert werden - eine vorgefertigte asynchrone Implementierung wurde im Internet gefunden. Hier werden wir es verwenden.

Das Interessanteste ist in der Klasse CounterMQTTClient zusammengefasst, die auf der Bibliothek MQTTClient basiert. Beginnen wir an der Peripherie

 ##################################### # 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 Stifte von Glühbirnen und Knöpfen sowie Objekte von Kalt- und Warmwasserzählern erstellt und konfiguriert.

Bei der Initialisierung ist nicht alles 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()) 

Um die Parameter der Bibliothek mqtt_as festzulegen, wird ein großes Wörterbuch mit verschiedenen Einstellungen verwendet - config. Die meisten Standardeinstellungen passen zu uns, aber viele Einstellungen müssen explizit festgelegt werden. Um die Einstellungen nicht direkt im Code zu registrieren, speichere ich sie in der Textdatei config.txt. Auf diese Weise können Sie den Code unabhängig von den Einstellungen ändern und mehrere identische Geräte mit unterschiedlichen Parametern nieten.

Der letzte Codeblock führt mehrere Coroutinen aus, um verschiedene Funktionen des Systems zu erfüllen. Hier ist ein Beispiel für eine Coroutine, die Zählern dient

  async def _counter_coro(self, counter, topic): # Publish initial value value = counter.value() await self.publish(topic, str(value)) # Publish each new value while True: value = await counter await self.publish_msg(topic, str(value)) 

In einem Zyklus wartet Corutin auf einen neuen Zählerwert und sendet, sobald dieser angezeigt wird, eine Nachricht unter Verwendung des MQTT-Protokolls. Der erste Code sendet den Anfangswert, auch wenn das Wasser nicht durch den Zähler fließt.

Die MQTTClient-Basisklasse bedient sich selbst, initiiert eine WiFi-Verbindung und stellt die Verbindung wieder her, wenn die Verbindung unterbrochen wird. Wenn sich der WiFi-Verbindungsstatus ändert, informiert uns die Bibliothek durch Aufrufen von wifi_connection_handler

  async def wifi_connection_handler(self, state): self.internet_outage = not state if state: self.dprint('WiFi is up.') duration = ticks_diff(ticks_ms(), self.internet_outage_start) // 1000 await self.publish_debug_msg('ReconnectedAfter', duration) else: self.internet_outages += 1 self.internet_outage_start = ticks_ms() self.dprint('WiFi is down.') await asyncio.sleep(0) 

Die Funktion wird ehrlich aus den Beispielen geleckt. In diesem Fall werden die Anzahl der Verbindungsabbrüche (internet_outages) und deren Dauer berücksichtigt. Wenn eine Verbindung wiederhergestellt wird, werden Ausfallzeiten an den Server gesendet.

Übrigens wird der letzte Schlaf nur benötigt, damit die Funktion asynchron wird - in der Bibliothek wird sie über await aufgerufen, und nur Funktionen, in deren Körper sich ein weiteres Warten befindet, können aufgerufen werden.

Neben der Verbindung zu WiFi müssen Sie auch eine Verbindung zum MQTT-Broker (Server) herstellen. Die Bibliothek tut dies auch, aber wir haben die Möglichkeit, etwas Nützliches zu tun, wenn die Verbindung hergestellt wird.

  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ählerwerte durch Senden der entsprechenden Nachricht festzulegen.

  def mqtt_msg_handler(self, topic, msg): topicstr = str(topic, 'utf8') self.dprint("Received MQTT message 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 eingehende Nachrichten und abhängig vom Thema (Nachrichtenname) 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 Nachrichten, wenn eine Verbindung hergestellt wird. Wenn keine Verbindung besteht, wird die Nachricht ignoriert.

Und dies ist nur eine praktische Funktion, die Debugging-Nachrichten generiert 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, aber wir haben keine LED geblinkt. Hier

  # 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 bereitgestellt. Wenn die Verbindung unterbrochen wird (oder gerade hergestellt wird), blinkt das Gerät schnell. Wenn die Verbindung hergestellt ist, blinkt das Gerät alle 5 Sekunden. Bei Bedarf können Sie hier andere Blinkmodi implementieren.

Aber die LED verwöhnt so. Wir winkten immer noch zum Display.

  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) 

Darüber habe ich gesprochen - wie einfach und bequem mit Coroutinen. Diese kleine Funktion beschreibt ALLE Benutzerinteraktionen. Corutin wartet nur auf das Drücken einer Taste und schaltet das Display 3 Sekunden lang ein. Das Display zeigt die aktuellen Zählerstände an.

Es gibt noch ein paar Kleinigkeiten. Hier ist die Funktion, die die gesamte Farm (re) ausführt. Der Hauptzyklus behandelt nur das Senden verschiedener Debugging-Informationen einmal pro Minute. Im Allgemeinen zitiere ich so wie es ist - insbesondere ein Kommentar, denke ich, ist nicht notwendig

  async def main(self): while True: try: await self._connect_to_WiFi() await self._run_main_loop() except Exception as e: self.dprint('Global communication failure: ', e) await asyncio.sleep(20) async def _connect_to_WiFi(self): self.dprint('Connecting to WiFi and 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('Connected!') self.internet_outage = False async def _run_main_loop(self): # Loop forever mins = 0 while True: gc.collect() # For RAM stats. mem_free = gc.mem_free() mem_alloc = gc.mem_alloc() try: await self.publish_debug_msg("Uptime", mins) await self.publish_debug_msg("Repubs", self.REPUB_COUNT) await self.publish_debug_msg("Outages", self.internet_outages) await self.publish_debug_msg("MemFree", mem_free) await self.publish_debug_msg("MemAlloc", mem_alloc) except Exception as e: self.dprint("Exception occurred: ", e) mins += 1 await asyncio.sleep(60) 

Nun, ein paar weitere Einstellungen und Konstanten, um die Beschreibung zu vervollständigen

 ##################################### # 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) 

Es beginnt so

 client = CounterMQTTClient() loop = asyncio.get_event_loop() loop.run_until_complete(client.main()) 


Etwas mit meiner Erinnerung ist geworden


Der gesamte Code ist also vorhanden. Ich habe die Dateien mit dem Dienstprogramm ampy hochgeladen. Dadurch können sie auf das interne Flash-Laufwerk (das in ESP-07 selbst enthalten ist) hochgeladen und dann vom Programm aus wie normale Dateien aufgerufen werden. Dort habe ich die von mir verwendeten Bibliotheken mqtt_as, uasyncio, ssd1306 und Sammlungen (in mqtt_as verwendet) hochgeladen.

Wir starten und ... Wir erhalten MemoryError. Je mehr ich versuchte, genau zu verstehen, wo der Speicher leckte, desto mehr arrangierte ich das Debuggen von Ausdrucken, desto früher trat dieser Fehler auf. Ein kurzer Gugelezh führte mich zu dem Verständnis, dass der Mikrocontroller im Prinzip nur 30 KB Speicher hat, in den 65 KB Code (zusammen mit Bibliotheken) in keiner Weise passen.

Aber es gibt einen Weg. Es stellt sich heraus, dass Micropython keinen Code direkt aus der .py-Datei ausführt - diese Datei wird zuerst kompiliert. Und es kompiliert direkt auf dem Mikrocontroller, verwandelt sich in Bytecode, der dann im Speicher gespeichert wird. Damit der Compiler funktioniert, benötigen Sie außerdem eine bestimmte Menge an RAM.

Der Trick besteht darin, den Mikrocontroller vor einer ressourcenintensiven Kompilierung zu schützen. Sie können Dateien auf einem großen Computer kompilieren und den fertigen Bytecode in den Mikrocontroller eingeben. Laden Sie dazu die Micropython-Firmware herunter und erstellen Sie das Dienstprogramm mpy-cross .

Ich habe kein Makefile geschrieben, sondern alle erforderlichen Dateien (einschließlich Bibliotheken) wie folgt manuell zusammengestellt und kompiliert

 mpy-cross water_counter.py 

Es bleibt nur das Hochladen von Dateien mit der Erweiterung .mpy, nicht zu vergessen, zuerst die entsprechende .py aus dem Dateisystem des Geräts zu löschen.

Ich habe alle Entwicklungen im Programm (IDE?) ESPlorer durchgeführt. Sie können Skripte auf den Mikrocontroller hochladen und sofort ausführen. In meinem Fall befinden sich die gesamte Logik und Erstellung aller Objekte in der Datei water_counter.py (.mpy). Damit dies alles automatisch startet, muss zu Beginn eine andere Datei mit dem Namen main.py vorhanden sein. Und es sollte genau .py sein und keine vorkompilierte .mpy. Hier ist sein trivialer Inhalt

 import water_counter 

Wir fangen an - alles funktioniert. Aber es gibt gefährlich wenig freien Speicher - ungefähr 1 KB. Ich habe immer noch Pläne, die Funktionalität des Geräts zu erweitern, und dieser Kilobyte wird mir offensichtlich nicht ausreichen. Es stellte sich jedoch heraus, dass es für diesen Fall einen Ausweg gibt.

Hier ist das Ding. Obwohl die Dateien in Bytecode kompiliert sind und sich im internen Dateisystem befinden, werden sie tatsächlich immer noch in den RAM geladen und von dort ausgeführt. Es stellt sich jedoch heraus, dass Micropython Bytecode direkt aus dem Flash-Speicher ausführen kann. Dazu müssen Sie ihn jedoch direkt in die Firmware einbetten. Dies ist nicht schwierig, obwohl es auf meinem Netbook ziemlich lange gedauert hat (nur dort habe ich Linux).

Der Algorithmus ist wie folgt:

  • Laden Sie das ESP Open SDK herunter und installieren Sie es. Dieses Ding erstellt einen Compiler und Bibliotheken für Programme unter ESP8266. Es wird gemäß den Anweisungen auf der Hauptseite des Projekts zusammengestellt (ich habe die Einstellung STANDALONE = yes gewählt).
  • Laden Sie Micropython Sorts herunter
  • Legen Sie die erforderlichen Bibliotheken in den Ports / esp8266 / modules im Micropython-Baum ab
  • Wir bauen die Firmware gemäß den Anweisungen in der Datei ports / esp8266 / README.md zusammen
  • Gießen Sie die Firmware in den Mikrocontroller (ich mache dies unter Windows mit ESP8266Flasher-Programmen oder Python esptool)

Das war's, jetzt wird 'import ssd1306' den Code direkt von der Firmware abheben und RAM wird dafür nicht ausgegeben. Mit diesem Trick habe ich nur den Bibliothekscode in die Firmware heruntergeladen, während der Hauptprogrammcode aus dem Dateisystem ausgeführt wird. Auf diese Weise können Sie das Programm einfach ändern, ohne die Firmware neu zu kompilieren. Im Moment habe ich ca. 8,5kb RAM frei. Auf diese Weise können in Zukunft viele verschiedene nützliche Funktionen implementiert werden. Wenn nicht genügend Speicher vorhanden ist, können Sie das Hauptprogramm auch in die Firmware verschieben.

Und was soll ich jetzt damit machen?


Ok, die Hardware ist verlötet, die Firmware ist geschrieben, die Box ist gedruckt, das Gerät klebt an der Wand und blinkt freudig mit einer Glühbirne. Aber während dies alles eine Black Box ist (im wörtlichen und im übertragenen Sinne) und der Sinn davon immer noch nicht genug ist. Es ist Zeit, etwas mit MQTT-Nachrichten zu tun, die an den Server gesendet werden.

Mein „Smart Home“ dreht sich um das Majordomo-System . Das MQTT-Modul ist entweder sofort einsatzbereit oder kann einfach über den Add-On-Markt installiert werden. Ich weiß nicht mehr, woher es stammt. MQTT-Ding ist nicht autark - das sogenannte Broker - Ein Server, der MQTT-Nachrichten empfängt, sortiert und an Clients umleitet. Ich benutze Mücken, die (wie Majordomo) alle auf demselben Netbook laufen.

Nachdem das Gerät mindestens einmal eine Nachricht gesendet hat, wird der Wert sofort in der Liste angezeigt.



Diese Werte können jetzt Systemobjekten zugeordnet, in Automatisierungsskripten verwendet und verschiedenen Analysen unterzogen werden - all dies ist nicht Gegenstand dieses Artikels. Wer sich für das Majordomo-System interessiert, kann den Electronics In Lens-Kanal empfehlen - ein Freund baut auch ein intelligentes Haus und spricht verständlich über die Einrichtung des Systems.

Ich werde nur ein paar Grafiken zeigen. Dies ist ein einfaches Diagramm der Tageswerte.


Es ist zu sehen, dass fast niemand nachts Wasser benutzte. Ein paar Mal ging jemand auf die Toilette und es sieht so aus, als würde ein Umkehrosmosefilter ein paar Liter pro Nacht saugen. Am Morgen steigt der Verbrauch deutlich an. Normalerweise benutze ich Wasser aus einem Kessel, aber dann wollte ich ein Bad nehmen und vorübergehend auf Stadtheißwasser umstellen - dies ist auch in der unteren Grafik deutlich sichtbar.

Aus diesem Zeitplan habe ich gelernt, dass auf die Toilette gehen 6-7 Liter Wasser, Duschen - 20-30 Liter, Geschirr spülen ca. 20 Liter, und um ein Bad zu nehmen, braucht man 160 Liter. Für einen Tag verbraucht meine Familie ungefähr 500-600l.

Für die Neugierigsten können Sie sich die Einträge für jeden einzelnen Wert ansehen



Von hier aus erfuhr ich, dass bei geöffnetem Wasserhahn das Wasser mit einer Geschwindigkeit von etwa 1 Liter in 5 Sekunden fließt.

Aber in dieser Form sind Statistiken wahrscheinlich nicht sehr bequem anzusehen. In Majordomo besteht weiterhin die Möglichkeit, Verbrauchstabellen nach Tag, Woche und Monat anzuzeigen. Zum Beispiel ein Diagramm des Verbrauchs in Spalten



Bisher habe ich nur Daten für eine Woche. In einem Monat ist dieses Diagramm aussagekräftiger - eine separate Spalte entspricht jedem Tag. Anpassungen der von mir manuell eingegebenen Werte beeinträchtigen das Bild ein wenig (die größte Spalte). Und es ist noch nicht klar, ob ich die ersten Werte fast einen Würfel weniger falsch eingestellt habe oder ob dies ein Fehler in der Firmware ist und nicht alle Liter in die Aufrechnung gingen. Es dauert länger.

Es ist immer noch notwendig, über die Graphen selbst zu zaubern, weiß, Farbe. Vielleicht werde ich auch ein Diagramm des Speicherverbrauchs für Debugging-Zwecke erstellen - plötzlich tritt dort etwas aus. Vielleicht zeige ich irgendwie Zeiträume an, in denen das Internet nicht verfügbar war. Während sich das alles auf der Ebene der Ideen dreht.

Fazit


Heute ist meine Wohnung etwas schlauer geworden. Mit einem so kleinen Gerät ist es für mich bequemer, den Wasserverbrauch im Haus zu überwachen. Wenn ich früher empört war, "wieder haben sie in einem Monat viel Wasser verbraucht", kann ich jetzt die Quelle dieses Verbrauchs finden.

Es erscheint jemandem seltsam, die Messwerte auf dem Bildschirm zu sehen, wenn sie einen Meter vom Messgerät entfernt sind. Aber in nicht allzu ferner Zukunft plane ich, in eine andere Wohnung zu ziehen, in der es mehrere Steigleitungen geben wird und die Zähler selbst höchstwahrscheinlich auf dem Treppenabsatz liegen werden. Ein Fernlesegerät wäre also sehr hilfreich.

Ich plane auch, die Funktionalität des Geräts zu erweitern. Ich sehe mir bereits motorisierte Ventile an. Um jetzt das Wasser in der Kesselstadt zu wechseln, muss ich 3 Wasserhähne in einer unzugänglichen Nische drehen. Es wäre viel bequemer, dies mit einer Taste mit der entsprechenden Anzeige zu tun. Natürlich lohnt es sich, einen Leckschutz zu implementieren.

In dem Artikel habe ich meine Version des Geräts basierend auf ESP8266 erzählt. Meiner Meinung nach habe ich eine sehr interessante Version der Micropython-Firmware mit Coroutine erhalten - einfach und hübsch.Ich habe versucht, die vielen Nuancen und Schulen zu beschreiben, denen ich bei der Kampagne begegnet bin. Vielleicht habe ich alles zu detailliert beschrieben, für mich persönlich ist es als Leser einfacher, zu viel zu verschwenden, als sich dann auszudenken, was nicht gesagt wurde.

Wie immer bin ich offen für konstruktive Kritik.

Quellcode-
Schaltungs- und
Platinengehäusemodell

Source: https://habr.com/ru/post/de411259/


All Articles