Wir verbinden den WasserzÀhler mit dem Smart Home

FrĂŒher waren Hausautomationssysteme, oder wie sie oft als „Smart Home“ bezeichnet werden, furchtbar teuer und nur die Reichen konnten sie sich leisten. Heutzutage findet man auf dem Markt recht preisgĂŒnstige BausĂ€tze mit Sensoren, Tastern/Schaltern und Aktoren zur Steuerung von Beleuchtung, Steckdosen, LĂŒftung, Wasserversorgung und anderen Verbrauchern. Und selbst der krummste Heimwerker kann sich der Schönheit anschließen und zu einem gĂŒnstigen Preis GerĂ€te fĂŒr ein Smart Home zusammenbauen.

Wir verbinden den WasserzÀhler mit dem Smart Home

In der Regel handelt es sich bei den vorgeschlagenen GerĂ€ten entweder um Sensoren oder Aktoren. Damit lassen sich Szenarien wie „Wenn der Bewegungsmelder auslöst, Licht einschalten“ oder „Der Schalter in der NĂ€he des Ausgangs schaltet das Licht in der gesamten Wohnung aus“ einfach umsetzen. Aber irgendwie hat es mit der Telemetrie nicht geklappt. Im besten Fall handelt es sich dabei um ein Diagramm der Temperatur und Luftfeuchtigkeit oder der Momentanleistung in einer bestimmten Steckdose.

Ich habe kĂŒrzlich WasserzĂ€hler mit Impulsausgang installiert. Mit jedem Liter, der durch den ZĂ€hler gelaufen ist, wird der Reed-Schalter aktiviert und schließt den Kontakt. Das Einzige, was noch zu tun bleibt, ist, sich an den DrĂ€hten festzuhalten und zu versuchen, einen Nutzen daraus zu ziehen. Analysieren Sie beispielsweise den Wasserverbrauch nach Stunden und Wochentagen. Nun, wenn es in der Wohnung mehrere Steigleitungen fĂŒr Wasser gibt, ist es bequemer, alle aktuellen Indikatoren auf einem Bildschirm zu sehen, als mit einer Taschenlampe schwer zugĂ€ngliche Nischen zu erklimmen.

Unter dem Schnitt meine Version eines auf ESP8266 basierenden GerĂ€ts, das Impulse von WasserzĂ€hlern zĂ€hlt und die Messwerte per MQTT an den Smart-Home-Server sendet. Wir werden in Micropython mit der Uasyncio-Bibliothek programmieren. Bei der Erstellung der Firmware bin ich auf einige interessante Schwierigkeiten gestoßen, auf die ich auch in diesem Artikel eingehen werde. Gehen!

Fahren

Wir verbinden den WasserzÀhler mit dem Smart Home

Das HerzstĂŒck der gesamten Schaltung ist ein Modul auf dem ESP8266-Mikrocontroller. UrsprĂŒnglich war ESP-12 geplant, aber meiner erwies sich als defekt. Ich musste mich mit dem verfĂŒgbaren ESP-07-Modul begnĂŒgen. GlĂŒcklicherweise sind sie sowohl in Bezug auf die Schlussfolgerungen als auch auf die FunktionalitĂ€t gleich, der einzige Unterschied besteht in der Antenne – beim ESP-12 ist sie eingebaut, wĂ€hrend sie beim ESP-07 ĂŒber eine externe verfĂŒgt. Aber auch ohne WLAN-Antenne wird das Signal in meinem Badezimmer normal empfangen.

Die Anbindung des Moduls ist Standard:

  • Reset-Knopf mit Pull-up und Kondensator (obwohl sich beide bereits im Modul befinden)
  • Das Freigabesignal (CH_PD) wird auf Strom gesetzt
  • GPIO15 auf Masse gezogen. Das ist nur am Anfang nötig, aber ich muss mich trotzdem nicht mehr an diesem Bein festhalten

Um das Modul in den Firmware-Modus zu versetzen, mĂŒssen Sie GPIO2 mit dem Boden verbinden. Um es bequemer zu machen, habe ich die Boot-Taste bereitgestellt. Im Normalzustand liegt an diesem Pin Spannung an.

Der Zustand der GPIO2-Leitung wird nur zu Beginn des Betriebs ĂŒberprĂŒft – beim Anlegen der Spannung oder unmittelbar nach einem Reset. Das Modul bootet also entweder wie gewohnt oder wechselt in den Firmware-Modus. Nach dem Laden kann dieser Pin als regulĂ€rer GPIO verwendet werden. Nun, da dort bereits ein Knopf vorhanden ist, können Sie daran eine nĂŒtzliche Funktion hĂ€ngen.

Zum Programmieren und Debuggen verwende ich den UART, den ich zum Kamm gebracht habe. Bei Bedarf schließe ich dort einfach einen USB-UART-Adapter an. Sie mĂŒssen nur bedenken, dass das Modul mit 3.3 V betrieben wird. Wenn Sie vergessen, den Adapter auf diese Spannung umzustellen und 5 V anzulegen, wird das Modul höchstwahrscheinlich durchbrennen.

Ich habe keine Probleme mit der ElektrizitĂ€t im Badezimmer – die Steckdose befindet sich etwa einen Meter von den ZĂ€hlern entfernt, daher werde ich sie mit 220 V versorgen. Als Stromquelle werde ich eine kleine haben Block HLK-PM03 von Tenstar Robot. Persönlich tue ich mich mit Analog- und Leistungselektronik schwer, und hier ein fertiges Netzteil im kleinen GehĂ€use.

Zur Signalisierung der BetriebszustĂ€nde habe ich eine an GPIO2 angeschlossene LED vorgesehen. Allerdings habe ich es nicht gelötet, weil. Das ESP-07-Modul verfĂŒgt bereits ĂŒber eine LED, die an denselben GPIO2 angeschlossen ist. Aber lass es auf der Platine sein – plötzlich möchte ich diese LED ins GehĂ€use bringen.

Kommen wir zum Interessantesten. WasserzĂ€hler haben keine Logik, sie 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. Ich habe die Reed-Schalter-AusgĂ€nge in GPIO12 / GPIO13. Ich werde den Pull-up-Widerstand im Modul programmgesteuert einschalten.

Anfangs habe ich vergessen, die WiderstĂ€nde R8 und R9 vorzusehen, und diese sind in meiner Version der Platine nicht enthalten. Aber da ich das Schema bereits fĂŒr alle sichtbar darlege, lohnt es sich, dieses Versehen zu korrigieren. WiderstĂ€nde werden benötigt, um den Port nicht zu verbrennen, wenn die Firmware fehlerhaft ist und eine Einheit auf den Pin setzt, und der Reed-Schalter diese Leitung mit Masse kurzschließt (mit einem Widerstand fließen maximal 3.3 V / 1000 Ω = 3.3 mA). .

Es ist Zeit, darĂŒber nachzudenken, was zu tun ist, wenn der Strom ausfĂ€llt. Die erste Möglichkeit besteht darin, beim Start den Server nach den Anfangswerten der ZĂ€hler zu fragen. Dies wĂŒrde jedoch eine erhebliche Komplikation des Austauschprotokolls erfordern. DarĂŒber hinaus hĂ€ngt die Leistung des GerĂ€ts in diesem Fall vom Zustand des Servers ab. Wenn der Server nach dem Ausschalten des Lichts nicht startete (oder spĂ€ter startete), konnte der WasserzĂ€hler die Anfangswerte nicht anfordern und funktionierte fehlerhaft.

Daher habe ich mich entschieden, die Speicherung der ZĂ€hlerwerte in einem ĂŒber I2C angeschlossenen Speicherchip zu implementieren. Ich habe keine besonderen Anforderungen an die GrĂ¶ĂŸe des Flash-Speichers – Sie mĂŒssen nur 2 Zahlen speichern (die Anzahl der Liter gemĂ€ĂŸ Warm- und KaltwasserzĂ€hlern). Auch das kleinste Modul reicht aus. Sie mĂŒssen jedoch auf die Anzahl der Schreibzyklen achten. Bei den meisten Modulen sind es 100 Zyklen, bei einigen sogar bis zu einer Million.

Es scheint, dass eine Million eine Menge ist. Aber in den vier Jahren, in denen ich in meiner Wohnung gelebt habe, habe ich etwas mehr als 4 Kubikmeter Wasser verbraucht, das sind 500 Liter! Und 500 DatensĂ€tze im Flash. Und das ist nur kaltes Wasser. Sie können den Chip natĂŒrlich alle paar Jahre neu löten, aber es stellte sich heraus, dass es sich um FRAM-Chips handelte. Aus programmtechnischer Sicht ist dies das gleiche I500C-EEPROM, nur mit einer sehr großen Anzahl von Überschreibzyklen (Hunderte Millionen). Bis ich mit solchen Mikroschaltungen immer noch nicht in den Laden komme, bleibt also vorerst der ĂŒbliche 2LC24 stehen.

Platine

UrsprĂŒnglich hatte ich vor, zu Hause ein Brett zu basteln. Daher wurde die Platine einseitig gestaltet. Aber nachdem ich eine Stunde mit einem Laserkolben und einer Lötmaske verbracht hatte (ohne die geht es irgendwie nicht), entschloss ich mich trotzdem, Platinen bei den Chinesen zu bestellen.

Wir verbinden den WasserzÀhler mit dem Smart Home

Kurz bevor ich das Board bestellt habe, wurde mir klar, dass man neben dem Flash-Speicherchip noch etwas anderes NĂŒtzliches an den I2C-Bus anschließen kann, zum Beispiel ein Display. Was genau ausgegeben werden soll, ist noch eine Frage, aber Sie mĂŒssen es auf der Tafel zĂŒchten. Nun, da ich Platinen im Werk bestellen wollte, hatte es keinen Sinn, mich auf eine einseitige Platine zu beschrĂ€nken, daher sind die I2C-Leitungen die einzigen auf der RĂŒckseite der Platine.

Ein großer Pfosten war ebenfalls mit der Einwegverkabelung verbunden. Weil Die Platine wurde einseitig gezeichnet, dann sollten auf der einen Seite die Leiterbahnen und SMD-Bauteile und auf der anderen die Ausgangsbauteile, AnschlĂŒsse und die Stromversorgung platziert werden. Als ich die Platinen einen Monat spĂ€ter erhielt, vergaß ich den ursprĂŒnglichen Plan und lötete alle Bauteile auf der Vorderseite fest. Und erst beim Anlöten des Netzteils stellte sich heraus, dass Plus und Minus umgekehrt getrennt waren. Ich musste mit Pullovern arbeiten. Im Bild oben habe ich die Verkabelung bereits geĂ€ndert, aber die Masse wird ĂŒber die Pins des Boot-Buttons von einem Teil der Platine auf einen anderen ĂŒbertragen (obwohl es möglich wĂ€re, auf der zweiten Ebene eine Spur zu zeichnen).

Es ist so gekommen

Wir verbinden den WasserzÀhler mit dem Smart Home

GehÀuse

Der nĂ€chste Schritt ist der Körper. Wenn Sie einen 3D-Drucker haben, ist dies kein Problem. Ich habe mir nicht viel MĂŒhe gegeben – ich habe einfach eine Schachtel in der richtigen GrĂ¶ĂŸe gezeichnet und an den richtigen Stellen Ausschnitte gemacht. Die Abdeckung wird mit kleinen selbstschneidenden Schrauben am GehĂ€use befestigt.

Wir verbinden den WasserzÀhler mit dem Smart Home

Ich habe bereits erwĂ€hnt, dass die Boot-Taste als Allzwecktaste verwendet werden kann – bringen wir sie also auf die Vorderseite. Dazu habe ich einen speziellen „Brunnen“ gezeichnet, in dem sich der Knopf befindet.

Wir verbinden den WasserzÀhler mit dem Smart Home

Im Inneren des GehĂ€uses befinden sich außerdem Stutzen, auf denen die Platine installiert und mit einer einzigen M3-Schraube befestigt wird (auf der Platine war kein Platz mehr)

Das Display war bereits ausgewĂ€hlt, als ich die erste passende Version des GehĂ€uses gedruckt habe. Ein Standard-Zweizeilendrucker passte nicht in dieses GehĂ€use, aber im Boden des Zylinders befand sich ein OLED-Display SSD1306 128 × 32. Es ist klein, aber ich starre ihn nicht jeden Tag an – es rollt.

Nachdem ich so und so abgeschĂ€tzt hatte, wie die DrĂ€hte daraus verlegt werden wĂŒrden, beschloss ich, das Display in die Mitte des GehĂ€uses zu kleben. Ergonomie natĂŒrlich unterhalb des Sockels – der Knopf ist oben, das Display unten. Aber ich habe bereits gesagt, dass die Idee, das Display zu verschrauben, zu spĂ€t kam und ich zu faul war, die Platine neu zu verkabeln, um den Knopf zu bewegen.

Zusammengebautes GerĂ€t. Das Displaymodul wird mit Heißkleber auf den Rotz geklebt

Wir verbinden den WasserzÀhler mit dem Smart Home

Wir verbinden den WasserzÀhler mit dem Smart Home

Das Endergebnis ist auf KDPV zu sehen

EinfĂŒgung

Kommen wir zum Softwareteil. FĂŒr so kleine Bastelarbeiten verwende ich sehr gerne die Python-Sprache (Mikropython) - der Code ist sehr kompakt und verstĂ€ndlich. GlĂŒcklicherweise ist es nicht nötig, bis auf die Registerebene vorzudringen, um Mikrosekunden herauszuquetschen – alles kann von Python aus erledigt werden.

Es scheint, dass alles einfach ist, aber nicht sehr – das GerĂ€t verfĂŒgt ĂŒber mehrere unabhĂ€ngige Funktionen:

  • Der Benutzer tippt auf eine SchaltflĂ€che und blickt auf das Display
  • Liter ticken und aktualisieren Werte im Flash-Speicher
  • Das Modul ĂŒberwacht das WLAN-Signal und stellt bei Bedarf eine erneute Verbindung her
  • Nun, ohne eine blinkende GlĂŒhbirne geht es ĂŒberhaupt nicht

Es ist unmöglich zuzugeben, dass eine Funktion nicht funktioniert hat, wenn die andere aus irgendeinem Grund dumm ist. Ich habe in anderen Projekten bereits Kakteen gegessen und jetzt sehe ich immer noch Störungen wie „einen weiteren Liter verpasst, weil die Anzeige gerade aktualisiert wurde“ oder „der Benutzer kann nichts tun, wĂ€hrend das Modul eine Verbindung zum WLAN herstellt“. NatĂŒrlich können einige Dinge durch Interrupts erledigt werden, es kann jedoch zu EinschrĂ€nkungen hinsichtlich der Dauer, der Verschachtelung von Aufrufen oder der nichtatomaren Änderung von Variablen kommen. Nun, der Code, der alles erledigt und sofort schnell zu einem Chaos wird.

В ernsteres Projekt Ich habe klassisches prĂ€emptives Multitasking und FreeRTOS verwendet, aber in diesem Fall erwies sich das Modell als viel geeigneter Coroutinen und Uasync-Bibliotheken . DarĂŒber hinaus ist die Python-Implementierung von Coroutinen einfach eine Bombe – alles ist fĂŒr den Programmierer einfach und bequem erledigt. Schreiben Sie einfach Ihre eigene Logik und sagen Sie mir einfach, wo Sie zwischen Threads wechseln können.

Ich schlage vor, die Unterschiede zwischen prÀemptivem und kompetitivem 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 verwaltet. ZunĂ€chst wird der Anfangswert des ZĂ€hlers vom EEPROM (value_storage) subtrahiert – so wird die Wiederherstellung nach einem Stromausfall realisiert.

Der Pin wird mit einem eingebauten Pull-Up zur Stromversorgung initialisiert: Wenn der Reed-Schalter geschlossen ist, ist die Leitung Null, wenn die Leitung offen ist, wird er zur Stromversorgung hochgezogen und der Controller zeigt Eins an.

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)

Eine Verzögerung von 25 ms ist erforderlich, um das Abprallen von Kontakten zu filtern, und reguliert gleichzeitig, 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 Reed-Schalterkontakte geschlossen sind, dann 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 lediglich. Nun, es wĂ€re schön, einen neuen Wert auf einen USB-Stick zu schreiben.

Zur Vereinfachung der Verwendung werden „Zubehörteile“ 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 nun die VorzĂŒge von Python und der uasync-Bibliothek und machen das ZĂ€hlerobjekt wartebar (wie kann ich es ins Russische ĂŒbersetzen? Das, was erwartet werden kann?)

    def __await__(self):
        while not self._value_changed:
            yield from asyncio.sleep(0)

        return self.value()

    __iter__ = __await__  

Dies ist eine sehr praktische Funktion, die wartet, bis der ZĂ€hlerwert aktualisiert wird – die Funktion wacht von Zeit zu Zeit auf und ĂŒberprĂŒft das Flag _value_changed. Der Trick dieser Funktion besteht darin, dass der aufrufende Code bei einem Aufruf dieser Funktion in den Ruhezustand wechseln kann, bis ein neuer Wert empfangen wird.

Aber was ist mit Unterbrechungen?Ja, an dieser Stelle können Sie mich trollen und sagen, dass er selbst von Unterbrechungen gesprochen hat, aber tatsĂ€chlich hat er eine dumme Pin-Umfrage arrangiert. TatsĂ€chlich sind Interrupts das erste, was ich versucht habe. Im ESP8266 können Sie einen Interrupt auf der Vorderseite organisieren und sogar einen Interrupt-Handler fĂŒr diesen Interrupt in Python schreiben. In diesem Interrupt können Sie den Wert einer Variablen aktualisieren. Dies wĂŒrde wahrscheinlich ausreichen, wenn der ZĂ€hler ein Slave-GerĂ€t wĂ€re, das wartet, bis er nach diesem Wert gefragt wird.

Leider (oder zum GlĂŒck?) ist mein GerĂ€t aktiv, es sollte selbst Nachrichten ĂŒber das MQTT-Protokoll senden und Daten ins EEPROM schreiben. Und hier kommen bereits EinschrĂ€nkungen ins Spiel: Sie können keinen Speicher in Interrupts zuweisen und keinen großen Stapel 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 „so schnell und sofort“ ausfĂŒhren können, aber es stellt sich die Frage: „Was ist der Sinn?“ Plötzlich senden wir gerade eine Nachricht, und dann bricht ein Interrupt ein und verfĂ€lscht die Werte von Variablen. Oder es ist beispielsweise ein neuer ZĂ€hlerwert vom Server eingetroffen, wĂ€hrend wir den alten noch nicht erfasst haben. Im Allgemeinen mĂŒssen Sie die Synchronisierung blockieren oder auf andere Weise herauskommen.

Und von Zeit zu Zeit stĂŒrzt RuntimeError: Schedule Stack Full ab und wer weiß warum?

Mit expliziter Abfrage 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)

Es ist schwierig, direkt in Python mit Bytes zu arbeiten, und es sind Bytes, die in den Speicher geschrieben werden. Ich musste die Konvertierung zwischen einer Ganzzahl und Bytes mithilfe der ustruct-Bibliothek abgrenzen.

Um nicht jedes Mal das I2C-Objekt und die Adresse der Speicherzelle zu ĂŒbertragen, habe ich das Ganze in einen kleinen, 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)

Mit diesen Parametern wird das I2C-Objekt selbst 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, Sie mĂŒssen das Protokoll selbst nicht implementieren – ich habe es im Internet gefunden vorgefertigte asynchrone Implementierung. Hier werden wir es verwenden.

Das Interessanteste ist in der CounterMQTTClient-Klasse gesammelt, die auf der Bibliothek MQTTClient basiert. Beginnen wir mit 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 GlĂŒhbirnen- und Tasterstifte sowie Kalt- und WarmwasserzĂ€hlerobjekte 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 mqtt_as-Bibliothek festzulegen, wird ein großes Wörterbuch mit verschiedenen Einstellungen verwendet – config. Die meisten Standardeinstellungen funktionieren fĂŒr uns, viele Einstellungen mĂŒssen jedoch explizit festgelegt werden. Um die Einstellungen nicht direkt im Code vorzuschreiben, speichere ich diese in einer Textdatei config.txt. Dadurch können Sie den Code unabhĂ€ngig von den Einstellungen Ă€ndern sowie mehrere identische GerĂ€te mit unterschiedlichen Parametern vernieten.

Der letzte Codeblock startet mehrere Coroutinen, die verschiedene Systemfunktionen bedienen. Hier ist ein Beispiel fĂŒr eine Coroutine, die ZĂ€hler bedient

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

Die Coroutine wartet in einer Schleife auf einen neuen ZĂ€hlerwert und sobald dieser erscheint, sendet sie eine Nachricht ĂŒber das MQTT-Protokoll. Der erste Codeteil sendet den Anfangswert auch dann, wenn kein Wasser durch den ZĂ€hler fließt.

Die Basisklasse MQTTClient bedient sich selbst, initiiert eine WLAN-Verbindung und stellt die Verbindung wieder her, wenn die Verbindung verloren geht. Wenn sich der Status der WLAN-Verbindung Àndert, informiert uns die 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 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 ist den Beispielen ehrlich entnommen. In diesem Fall zÀhlt es die Anzahl der AusfÀlle (internet_outages) und deren Dauer. Wenn die Verbindung wiederhergestellt ist, wird eine Leerlaufzeit an den Server gesendet.

Der letzte Ruhezustand wird ĂŒbrigens nur benötigt, damit die Funktion asynchron wird. In der Bibliothek wird er ĂŒber „await“ aufgerufen, und es können nur Funktionen aufgerufen werden, in deren Rumpf sich ein weiterer „await“ befindet.

ZusĂ€tzlich zur WLAN-Verbindung mĂŒssen Sie auch eine Verbindung mit dem MQTT-Broker (Server) herstellen. Dies ĂŒbernimmt auch die Bibliothek, und wir bekommen die Möglichkeit, etwas NĂŒtzliches zu tun, wenn 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 nun die Möglichkeit, die aktuellen Werte der ZĂ€hler 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 je nach Betreff (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 ist dafĂŒr verantwortlich, eine Nachricht zu senden, wenn die Verbindung hergestellt wird. Wenn keine Verbindung besteht, wird die Nachricht ignoriert.

Und das ist nur eine praktische Funktion, die Debug-Meldungen 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 und wir haben die LED noch nicht zum Blinken gebracht. 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 zwei Blinkmodi bereitgestellt. Wenn die Verbindung verloren geht (oder gerade aufgebaut wird), blinkt das GerÀt schnell. Wenn die Verbindung hergestellt ist, blinkt das GerÀt alle 2 Sekunden. Bei Bedarf können hier auch andere Blinkmodi implementiert werden.

Aber die LED ist so, verwöhnend. Wir haben uns auch die Auslage angeschaut.

    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 es mit Coroutinen ist. Diese kleine Funktion beschreibt ALLE Benutzerinteraktionen. Die Coroutine wartet einfach auf das DrĂŒcken der Taste und schaltet das Display fĂŒr 3 Sekunden ein. Das Display zeigt die aktuellen ZĂ€hlerstĂ€nde an.

Es sind noch ein paar Kleinigkeiten ĂŒbrig. Hier ist die Funktion, die diese ganze Wirtschaft (neu) startet. In der Hauptschleife geht es nur darum, einmal pro Minute verschiedene Debugging-Informationen zu senden. Im Allgemeinen gebe ich es so, wie es ist – ich denke, ich brauche keinen besonderen Kommentar

   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, der VollstÀndigkeit der Beschreibung halber noch ein paar Einstellungen und Konstanten

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

Alles beginnt so

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

Etwas ist mit meiner Erinnerung passiert

Der gesamte Code ist also vorhanden. Ich habe die Dateien mit dem Dienstprogramm ampy hochgeladen. Damit können Sie sie auf das interne Flash-Laufwerk (das im ESP-07 selbst) hochladen und dann vom Programm aus wie normale Dateien darauf zugreifen. Dort habe ich auch die Bibliotheken mqtt_as, uasyncio, ssd1306 und Sammlungen hochgeladen, die ich verwendet habe (in mqtt_as verwendet).

Wir starten und... Wir erhalten MemoryError. DarĂŒber hinaus trat dieser Fehler umso frĂŒher auf, je mehr ich versuchte, genau zu verstehen, wo der Speicherverlust lag, je mehr ich die Ausdrucke debuggte. Eine kurze Google-Suche brachte mich zu der Erkenntnis, dass im Mikrocontroller grundsĂ€tzlich nur 30 KB Speicher vorhanden sind, in den 65 KB Code (zusammen mit Bibliotheken) ĂŒberhaupt nicht passen.

Aber es gibt einen Ausweg. Es stellt sich heraus, dass Micropython den Code nicht direkt aus einer .py-Datei ausfĂŒhrt – diese Datei wird zuerst kompiliert. DarĂŒber hinaus wird es direkt auf dem Mikrocontroller kompiliert, in Bytecode umgewandelt, der dann im Speicher abgelegt wird. Nun, der Compiler benötigt auch eine gewisse Menge RAM, um zu funktionieren.

Der Trick besteht darin, den Mikrocontroller vor einer ressourcenintensiven Kompilierung zu bewahren. Sie können Dateien auf einem großen Computer kompilieren und vorgefertigten Bytecode auf den Mikrocontroller hochladen. Dazu mĂŒssen Sie die Micropython-Firmware herunterladen und erstellen mpy-cross-Dienstprogramm.

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

mpy-cross water_counter.py

Es mĂŒssen nur noch die Dateien mit der Erweiterung .mpy ausgefĂŒllt werden. Denken Sie daran, zunĂ€chst die entsprechenden .py-Dateien aus dem Dateisystem des GerĂ€ts zu entfernen.

Ich habe die gesamte Entwicklung im Programm (IDE?) ESPlorer durchgefĂŒhrt. Es ermöglicht Ihnen, Skripte auf den Mikrocontroller hochzuladen und diese sofort auszufĂŒhren. In meinem Fall befindet sich die gesamte Logik und die Erstellung aller Objekte in der Datei „water_counter.py“ (.mpy). Damit das alles aber automatisch beim Start startet, muss auch eine Datei namens main.py vorhanden sein. DarĂŒber hinaus muss es genau .py sein und darf nicht vorkompiliert .mpy sein. Hier ist der triviale Inhalt

import water_counter

Wir fangen an – alles funktioniert. Aber der freie Speicher ist bedrohlich klein – etwa 1 KB. Ich habe noch PlĂ€ne, die FunktionalitĂ€t des GerĂ€ts zu erweitern, und dieses Kilobyte wird mir offensichtlich nicht ausreichen. Aber es stellte sich heraus, dass es einen Ausweg gibt.

Der Punkt ist dieser. Obwohl die Dateien in Bytecode kompiliert sind und sich im internen Dateisystem befinden, werden sie trotzdem 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 einbauen. Es ist nicht schwierig, obwohl es auf meinem Netbook ziemlich lange gedauert hat (nur dort hatte ich Linux).

Der Algorithmus lautet wie folgt:

  • Herunterladen und installieren ESP Open SDK. Dieses Ding erstellt einen Compiler und Bibliotheken fĂŒr Programme unter dem ESP8266. Es wird gemĂ€ĂŸ den Anweisungen auf der Hauptseite des Projekts zusammengestellt (ich habe die Einstellung STANDALONE=yes gewĂ€hlt)
  • Herunterladen Micropython-Sorten
  • Legen Sie die erforderlichen Bibliotheken in ports/esp8266/modules innerhalb des Micropython-Baums ab
  • Wir sammeln die Firmware gemĂ€ĂŸ den Anweisungen in der Datei ports/esp8266/README.md
  • Laden Sie die Firmware auf den Mikrocontroller hoch (ich mache das unter Windows mit den ESP8266Flasher-Programmen oder Pythons esptool)

Alles, jetzt hebt „ssd1306 importieren“ den Code direkt aus der Firmware hervor und RAM wird dafĂŒr nicht ausgegeben. Mit diesem Trick habe ich nur den Bibliothekscode in die Firmware hochgeladen, wĂ€hrend der Hauptprogrammcode aus dem Dateisystem ausgefĂŒhrt wird. Dadurch ist es einfach, das Programm zu Ă€ndern, ohne die Firmware neu kompilieren zu mĂŒssen. Im Moment habe ich etwa 8.5 KB RAM frei. Dies wird es uns ermöglichen, in Zukunft viele verschiedene nĂŒtzliche Funktionen zu implementieren. Wenn ĂŒberhaupt nicht genĂŒgend Speicher vorhanden ist, können Sie das Hauptprogramm in die Firmware ĂŒbertragen.

Und was tun jetzt damit?

Ok, das StĂŒck Eisen ist gelötet, die Firmware geschrieben, die Box bedruckt, das GerĂ€t an die Wand geklebt und das LĂ€mpchen blinkt fröhlich. Aber bisher ist das alles eine Blackbox (im wahrsten Sinne des Wortes) und es hat immer noch wenig Sinn. Es ist an der Zeit, etwas mit den MQTT-Nachrichten zu tun, die an den Server gesendet werden.

Mein „Smart Home“ dreht sich weiter 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 kommt. MQTT ist keine autarke Sache – Sie benötigen ein sogenanntes. Broker – ein Server, der Nachrichten annimmt, sortiert und an MQTT-Clients weiterleitet. Ich verwende Mosquito, das (wie Majordomo) auf demselben Netbook lĂ€uft.

Nachdem das GerÀt mindestens einmal eine Nachricht gesendet hat, erscheint der Wert sofort in der Liste.

Wir verbinden den WasserzÀhler mit dem Smart Home

Diese Werte können nun Systemobjekten zugeordnet werden, sie können in Automatisierungsskripten verwendet und verschiedenen Analysen unterzogen werden – all dies liegt außerhalb des Rahmens dieses Artikels. Wer sich fĂŒr das Majordomo-System interessiert, dem kann ich es empfehlen Kanalelektronik im Objektiv - Ein Freund baut ebenfalls ein Smart Home und spricht verstĂ€ndlich ĂŒber die Einrichtung des Systems.

Ich zeige Ihnen nur ein paar Grafiken. Dies ist ein einfaches Diagramm der Werte pro Tag

Wir verbinden den WasserzÀhler mit dem Smart Home
Es ist zu erkennen, dass nachts fast niemand das Wasser nutzte. Ein paar Mal ging jemand auf die Toilette und es sah so aus, als wĂŒrde der Umkehrosmosefilter jede Nacht ein paar Liter saugen. Am Morgen steigt der Verbrauch deutlich an. Normalerweise verwende ich Wasser aus dem Boiler, aber dann wollte ich baden und habe vorĂŒbergehend auf Stadtwarmwasser umgestellt – das ist auch in der unteren Grafik deutlich zu erkennen.

Aus dieser Tabelle habe ich gelernt, dass fĂŒr den Gang zur Toilette 6–7 Liter Wasser, fĂŒr das Duschen 20–30 Liter, fĂŒr das GeschirrspĂŒlen etwa 20 Liter und fĂŒr das Baden 160 Liter erforderlich sind. TagsĂŒber konsumiert meine Familie etwa 500-600l.

Wer besonders neugierig ist, kann sich die Aufzeichnungen fĂŒr jeden einzelnen Wert ansehen.

Wir verbinden den WasserzÀhler mit dem Smart Home

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

Aber in dieser Form sind die Statistiken wahrscheinlich nicht sehr praktisch anzusehen. Majordomo bietet außerdem die Möglichkeit, Verbrauchsdiagramme nach Tag, Woche und Monat anzuzeigen. Hier ist zum Beispiel eine grafische Darstellung des Verbrauchs in Spalten

Wir verbinden den WasserzÀhler mit dem Smart Home

Bisher habe ich nur Daten von einer Woche. In einem Monat wird diese Grafik aussagekrĂ€ftiger sein – fĂŒr jeden Tag gibt es eine eigene Spalte. Das Bild wird durch die Anpassungen der Werte, die ich manuell eingebe (die grĂ¶ĂŸte Spalte), etwas getrĂŒbt. Und es ist noch nicht klar, ob ich die allerersten Werte falsch eingestellt habe, fast einen WĂŒrfel weniger, oder ob das ein Fehler in der Firmware ist und nicht alle Liter berĂŒcksichtigt wurden. Brauche mehr Zeit.

Über den Diagrammen selbst mĂŒssen Sie noch zaubern, aufhellen und malen. Vielleicht werde ich zu Debugzwecken auch ein Diagramm des Speicherverbrauchs erstellen – plötzlich ist da etwas undicht. Vielleicht werde ich irgendwie die ZeitrĂ€ume darstellen, in denen es kein Internet gab. WĂ€hrend sich das alles auf der Ebene der Idee dreht.

Fazit

Heute ist meine Wohnung etwas smarter geworden. Mit einem so kleinen GerĂ€t kann ich den Wasserverbrauch im Haus bequemer ĂŒberwachen. War ich frĂŒher empört darĂŒber, „wieder wurde in einem Monat viel Wasser verbraucht“, kann ich jetzt die Quelle dieses Verbrauchs finden.

Es wird fĂŒr jemanden seltsam erscheinen, die Messwerte auf dem Bildschirm zu betrachten, wenn dieser einen Meter vom MessgerĂ€t selbst entfernt ist. Aber in nicht allzu ferner Zukunft habe ich vor, in eine andere Wohnung umzuziehen, wo es mehrere Wassersteigleitungen geben wird und die ZĂ€hler selbst höchstwahrscheinlich auf dem Treppenabsatz stehen werden. Ein FernlesegerĂ€t wĂ€re also sehr praktisch.

Ich habe auch vor, die FunktionalitĂ€t des GerĂ€ts zu erweitern. Ich schaue mir bereits motorisierte Ventile an. Um nun den Stadtwasserkessel zu wechseln, muss ich drei WasserhĂ€hne in einer schwer zugĂ€nglichen Nische aufdrehen. Viel bequemer wĂ€re es, dies mit einem Knopf mit entsprechender Anzeige zu tun. NatĂŒrlich lohnt es sich, einen Leckschutz zu implementieren.

In dem Artikel habe ich meine Version des auf ESP8266 basierenden GerĂ€ts beschrieben. Meiner Meinung nach habe ich mithilfe von Coroutinen eine sehr interessante Version der Micropython-Firmware erhalten – einfach und hĂŒbsch. Ich habe versucht, die vielen Nuancen und Unstimmigkeiten zu beschreiben, denen ich wĂ€hrend der Kampagne begegnet bin. Vielleicht habe ich alles zu ausfĂŒhrlich beschrieben, fĂŒr mich persönlich ist es als Leser einfacher, den Überschuss zu verschleudern, als spĂ€ter darĂŒber nachzudenken, was ungesagt geblieben ist.

FĂŒr konstruktive Kritik bin ich wie immer offen.

Quellcode
Schaltplan und Tafel
GehÀusemodell

Source: habr.com

Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz und VPS-VDS-Servern đŸ”„ Kaufen Sie zuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server | ProHoster