Anslut vattenmÀtaren till det smarta hemmet

En gÄng i tiden var hemautomatiseringssystem, eller som de ofta kallas "smart hem", fruktansvÀrt dyra och endast rika mÀnniskor hade rÄd med dem. Idag kan man hitta relativt överkomliga paket pÄ marknaden med sensorer, knappar/brytare och för aktiva enheter för att styra belysning, uttag, ventilation, vattenförsörjning och andra apparater. Och Àven den mest klumpiga gör-det-sjÀlv-entusiasten kan enkelt börja bygga smarta hem-enheter utan att sprÀnga sin budget.

Anslut vattenmÀtaren till det smarta hemmet

För det mesta Àr de erbjudna enheterna antingen sensorer eller aktuatorer. De möjliggör enkelt genomförande av scenarier som "nÀr rörelsesensorn aktiveras slÄs ljuset pÄ" eller "brytaren vid utgÄngen slÀcker ljuset i hela lÀgenheten". Men telemetry har inte riktigt funkat. I bÀsta fall rör det sig om en temperatur- och fuktighetsgraf eller omedelbar effekt i ett visst uttag.

Nyligen installerade jag vattenmÀtare med impulsausgÄng. För varje liter som passerar genom mÀtaren aktiveras en ferromagnet och stÀnger kontakten. Nu Àr det bara att ansluta kablarna och försöka fÄ nytta av det. Till exempel att analysera vattenförbrukningen per timme och veckodag. Om det finns flera rör för vatten i lÀgenheten Àr det enklare att se alla aktuella vÀrden pÄ en skÀrm istÀllet för att krypa i svÄrÄtkomliga nischer med en ficklampa.

Under rubriken hittar du min lösning baserad pÄ ESP8266, som rÀknar impulser frÄn vattenmÀtarna och skickar lÀsningar till smart hemservern via MQTT. Vi kommer att programmera i micropython med hjÀlp av uasyncio-biblioteket. NÀr jag skapade programvaran stötte jag pÄ nÄgra intressanta utmaningar, som jag ocksÄ kommer att berÀtta om i denna artikel. LÄt oss sÀtta igÄng!

Diagram

Anslut vattenmÀtaren till det smarta hemmet

HjĂ€rtat i hela schemat Ă€r modulen med mikrokontrollern ESP8266. Ursprungligen var ESP-12 planerad, men min var defekt. Jag fick nöja mig med modulen ESP-07 som fanns tillgĂ€nglig. Som tur Ă€r Ă€r de identiska bĂ„de vad gĂ€ller stift och funktionalitet, skillnaden ligger bara i antennen – ESP-12 har en inbyggd antenn, medan ESP-07 har en extern. Trots allt lyckas WiFi-signalen nĂ„ fram bra Ă€ven utan antenn i mitt badrum.

Anslutningen av modulen Àr standard:

  • reset-knapp med dragning och kondensator (Ă€ven om bĂ„de detta och det redan finns inuti modulen)
  • Signalen enable (CH_PD) Ă€r kopplad till strömförsörjningen
  • GPIO15 Ă€r kopplad till jord. Detta behövs endast vid start, men jag har inget mer att koppla till den pinnen

För att sÀtta modulen i programmeringslÀge mÄste GPIO2 kortslutas till jord, och för att göra det enklare har jag försett den med en Boot-knapp. Under normala omstÀndigheter dras denna pinne till ström.

TillstĂ„ndet för linjen GPIO2 kontrolleras endast i början av driften — nĂ€r strömmen tillförs eller omedelbart efter en reset. SĂ„ laddas modulen antingen som vanligt eller gĂ„r in i programmeringslĂ€ge. Efter laddning kan detta utgĂ„ng anvĂ€ndas som en vanlig GPIO. Och eftersom det redan finns en knapp kan jag koppla nĂ„gon anvĂ€ndbar funktion till den.

För programmering och felsökning kommer jag att anvÀnda UART, som jag har anslutit till skenan. NÀr det behövs kopplar jag bara in en USB-UART-adapter. Det gÀller bara att inte glömma att modulen drivs av 3.3V. Om jag glömmer att stÀlla in adaptern pÄ detta spÀnning och ger den 5V, sÄ kommer modulen troligtvis att brinna.

Jag har inga problem med elektricitet i badrummet — uttaget ligger ungefĂ€r en meter frĂ„n mĂ€tarna, sĂ„ jag kommer att anvĂ€nda 220V som strömförsörjning. Som strömförsörjning kommer jag att anvĂ€nda en liten block HLK-PM03 frĂ„n Tenstar Robot. Personligen har jag svĂ„rt med analog och kraftelektronik, sĂ„ hĂ€r Ă€r det en fĂ€rdig strömförsörjning i ett litet hölje.

För att signalera driftslĂ€gen har jag försett en LED kopplad till GPIO2. Jag har dock inte lött dit den, eftersom modulen ESP-07 redan har en LED, som dessutom Ă€r kopplad till samma GPIO2. Men pĂ„ kortet fĂ„r den gĂ€rna finnas — kanske jag vill dra ut denna LED pĂ„ höljet.

Nu kommer vi till det mest intressanta. VattenmĂ€tarna har ingen logik, man kan inte frĂ„ga om aktuella avlĂ€sningar. Det enda vi har tillgĂ„ng till Ă€r impulser — kontakter som sluter vid varje liter. UtgĂ„ngarna för reed-kontakterna Ă€r kopplade till GPIO12/GPIO13. Jag kommer att aktivera den pull-up-motstĂ„ndet programmĂ€ssigt inuti modulen.

Inledningsvis glömde jag att förutse motstÄnden R8 och R9 och i min version av kortet finns de inte. Men eftersom jag redan lÀgger upp schemat för allmÀnheten sÄ Àr det vÀrt att rÀtta till detta misstag. MotstÄnden behövs för att inte brÀnna porten i fall programmeringen strular och sÀtter en etta pÄ pinnen, medan reed-kontakten kortsluter denna linje till jord (med motstÄndet kommer högst 3.3V/1000Ω = 3.3mA att flöda).

Det Àr dags att tÀnka pÄ vad man ska göra om strömmen försvinner. Det första alternativet Àr att begÀra initialvÀrden för mÀtarna frÄn servern vid start. Men detta skulle krÀva en betydande förenkling av kommunikationsprotokollet. Dessutom beror enhetens funktionalitet i sÄ fall pÄ serverns tillstÄnd. Om servern inte startade efter strömavbrottet (eller startade senare), skulle vattenmÀtaren inte kunna begÀra initialvÀrden och skulle fungera felaktigt.

DĂ€rför beslutade jag att implementera lagring av mĂ€tvĂ€rden i ett minneschip anslutet via I2C. Jag har inga speciella krav pĂ„ storleken pĂ„ flashminnet — det cáș§n bara spara tvĂ„ nummer (antal liter för varmt och kallt vatten). Även den minsta modulen fungerar. Men vi mĂ„ste ta hĂ€nsyn till antalet skrivcykler. För de flesta moduler Ă€r det 100 tusen cykler, och för vissa upp till en miljon.

Det verkar som om en miljon Àr mycket. Men under mina fyra Är i min lÀgenhet har jag förbrukat lite mer Àn 500 kubikmeter vatten, det Àr 500 tusen liter! Och 500 tusen poster i flashminnet. Och det Àr bara kallt vatten. Man kan sjÀlvklart löda om chipet varannat Är, men det finns faktiskt FRAM-chips. Ur programmeringsperspektiv Àr det precis som I2C EEPROM, men med ett oÀndligt stort antal omskrivningscykler (hundratals miljoner). Men jag har fortfarande inte kommit ivÀg till affÀren med sÄdana chip, sÄ för tillfÀllet fÄr en vanlig 24LC512 duga.

Kretskort

Inledningsvis planerade jag att göra kortet hemma. DÀrför designades kortet som ett enkelsidigt kort. Men efter att ha kÀmpat i en timme med en laserstrykjÀrn och lödmask (utan den fungerar det inte riktigt bra), bestÀmde jag mig ÀndÄ för att bestÀlla kort frÄn Kina.

Anslut vattenmÀtaren till det smarta hemmet

Strax innan jag skulle bestĂ€lla kortet insĂ„g jag att utöver flashminneschipet kan jag Ă€ven koppla nĂ„got annat anvĂ€ndbart till I2C-bussen, som en skĂ€rm. Vad exakt som ska visas pĂ„ den — det Ă€r fortfarande en frĂ„ga, men det mĂ„ste göras pĂ„ kortet. Eftersom jag ska bestĂ€lla kortet frĂ„n fabriken var det egentligen meningslöst att begrĂ€nsa mig till ett enkelsidigt kort, sĂ„ I2C-linjerna Ă€r de enda pĂ„ baksidan av kortet.

Enkelriktad layout hade ocksÄ en stor nackdel. Eftersom kretskortet designades som en enkelriktad, var vÀgarna och SMD-komponenterna planerade att placeras pÄ ena sidan, medan utgÄende komponenter, kontakter och strömförsörjning skulle vara pÄ den andra sidan. NÀr jag efter en mÄnad fick korten, glömde jag den ursprungliga planen och lödde alla komponenter pÄ framsidan. Och först nÀr det var dags att löda strömförsörjningen upptÀckte jag att plus och minus var ombytta. Jag fick improvisera med jumpers. PÄ bilden ovan har jag redan Àndrat layouten, men jorden löper frÄn en del av kortet till en annan via knappen Boot (Àven om man kunde ha dragit en vÀg pÄ det andra lagret).

Det blev sÄ hÀr

Anslut vattenmÀtaren till det smarta hemmet

Chassi

NĂ€sta steg Ă€r chassit. Med en 3D-skrivare Ă€r det inte ett problem. Jag lade inte ner sĂ„ mycket tid pĂ„ det — jag ritade bara en lĂ„da av rĂ€tt storlek och gjorde urtag pĂ„ rĂ€tt stĂ€llen. Locket fĂ€sts i chassit med smĂ„ skruvar.

Anslut vattenmÀtaren till det smarta hemmet

Jag har redan nĂ€mnt att knappen Boot kan anvĂ€ndas som en allmĂ€n knapp — lĂ„t oss ta ut den pĂ„ framsidan. För detta ritade jag en speciell "brunn" dĂ€r knappen bor.

Anslut vattenmÀtaren till det smarta hemmet

Inuti chassit finns Àven smÄ utsprÄng dÀr kortet placeras och fÀsts med en enda M3-skruv (det fanns inte mer plats pÄ kortet).

Jag valde displayen först nĂ€r jag hade skrivit ut det första prototypchassit. Den standard tvĂ„radiga displayen fick inte plats, men jag hittade en OLED-display SSD1306 128×32 i förrĂ„det. Den Ă€r lite liten, men jag behöver inte titta pĂ„ den varje dag — den duger.

Med tanke pĂ„ hur kablarna skulle dras beslöt jag mig för att fĂ€sta displayen mitt i chassit. Ergonomin Ă€r sĂ„ klart under all kritik — knappen Ă€r upptill, displayen nedtill. Men jag har redan sagt att idĂ©n att sĂ€tta dit displayen kom för sent och jag hade inte lust att Ă€ndra layouten för att flytta knappen.

Enheten Àr ihopmonterad. Displaymodulen Àr fÀst med varm lim.

Anslut vattenmÀtaren till det smarta hemmet

Anslut vattenmÀtaren till det smarta hemmet

Det slutgiltiga resultatet kan ses pÄ KDPV

Firmware

LĂ„t oss gĂ„ vidare till programdelen. För sĂ„dana smĂ„ projekt gillar jag verkligen att anvĂ€nda sprĂ„ket Python (micropython)- koden blir mycket kompakt och lĂ€tt att förstĂ„. Det Ă€r bra eftersom det inte Ă€r nödvĂ€ndigt att gĂ„ ner till registernivĂ„ för att pressa mikrosekunder — allt kan göras frĂ„n Python.

Det verkar enkelt men Ă€r det inte – enheten ska ha flera oberoende funktioner:

  • AnvĂ€ndaren trycker pĂ„ knappen och tittar pĂ„ skĂ€rmen
  • Liter tickar och uppdaterar vĂ€rdena i flashminnet
  • Modulen övervakar WiFi-signalen och kopplar om vid behov
  • Och utan en blinkande lampa gĂ„r det överhuvudtaget inte

Det fÄr inte hÀnda att en funktion inte fungerar om en annan av nÄgon anledning strular. Jag har fÄtt nog av kaktusar i andra projekt och ser nu redan buggar i stil med "missade ytterligare en liter eftersom skÀrmen uppdaterades just dÄ" eller "anvÀndaren kan inte göra nÄgot medan modulen kopplar upp till WiFi". SjÀlvklart kan vissa saker göras via avbrott, men man kan stöta pÄ begrÀnsningar gÀllande varaktighet, anropens inbÀddning eller icke-atomÀra förÀndringar av variabler. Och kod som försöker göra allt pÄ en gÄng blir snabbt rörig.

I ett mer seriöst projekt jag anvĂ€nde klassisk preemptiv multitasking och FreeRTOS, men i det hĂ€r fallet visade modellen samarbetsprogram (coroutines) och biblioteket uasync . Dessutom Ă€r Python-implementeringen av korutiner helt fantastisk – för programmeraren Ă€r allt gjort enkelt och bekvĂ€mt. Skriv bara din logik och sĂ€g bara var man kan vĂ€xla mellan trĂ„darna.

Skillnaderna mellan preemptiv och konkurrerande multitasking föreslÄr jag att man studerar som valfritt. Men nu, lÄt oss Àntligen gÄ vidare till koden.

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

Varje rĂ€knare hanteras av en instans av klassen Counter. Först lĂ€ses det initiala vĂ€rdet frĂ„n EEPROM (value_storage) – detta implementerar Ă„terstĂ€llning efter strömförlust.

Pinnen initieras med inbyggd dragning till strömförsörjningen: om reed-strömbrytaren Ă€r sluten – noll pĂ„ linjen, om den Ă€r öppen dras linjen upp till strömförsörjningen och kontrolleraren lĂ€ser en etta.

Dessutom startas en separat uppgift som kommer att utföra avkÀnning av pinnen. Varje rÀknare kommer att starta sin egen uppgift. HÀr Àr dess kod

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

En fördröjning pÄ 25 ms behövs för att filtrera bort kontakter som skakar, och den reglerar ocksÄ hur ofta uppgiften vÀcks (medan denna uppgift sover, arbetar andra uppgifter). Var 25:e ms vÀcks funktionen, kontrollerar pinnen och om kontakterna pÄ hermetiska brytare Àr slutna, betyder det att nÀsta liter har passerat genom rÀknaren och detta behöver hanteras.

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

        self._value_storage.write(self._value)

Att hantera nĂ€sta liter Ă€r trivialt – rĂ€knaren ökas bara. Det nya vĂ€rdet skulle ocksĂ„ vara bra att spara pĂ„ flashminnet.

För anvÀndarvÀnlighet finns det "Ätkomstrutor".

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

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

Och nu ska vi anvÀnda skönheterna i Python och biblioteket uasync och göra rÀknarobjektet waitable (hur översÀtter man det till svenska? Den som kan vÀntas?)

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

        return self.value()

    __iter__ = __await__  

Detta Ă€r en bekvĂ€m funktion som vĂ€ntar tills rĂ€knarens vĂ€rde uppdateras – funktionen vĂ€cks dĂ„ och dĂ„ och kontrollerar flaggan _value_changed. Det som Ă€r fint med denna funktion Ă€r att den anropande koden kan sova medan den anropar denna funktion och sova tills den fĂ„r det nya vĂ€rdet.

Men vad med avbrott?Ja, hĂ€r kan ni trolla med mig och sĂ€ga att jag sjĂ€lv sa nĂ„got om avbrott, men egentligen satte jag upp en dum polling av pinnen. Egentligen Ă€r avbrott det första jag provade. I ESP8266 kan man organisera ett avbrott pĂ„ en front och till och med skriva en hanterare för det avbrottet i Python. I detta avbrott kan man uppdatera vĂ€rdet pĂ„ variabeln. Kanske skulle det rĂ€cka om rĂ€knaren var en slav-enhet – en som vĂ€ntar pĂ„ att nĂ„gon ska frĂ„ga efter detta vĂ€rde.

TyvĂ€rr (eller kanske lyckligtvis?) Ă€r min enhet aktiv, den ska sjĂ€lv skicka meddelanden via MQTT-protokollet och spara data i EEPROM. HĂ€r kommer begrĂ€nsningarna in — man kan inte allokera minne och anvĂ€nda stort stack i avbrott, vilket gör att vi kan glömma att skicka meddelanden över nĂ€tverket. Det finns fördelar som micropython.schedule() som lĂ„ter oss köra en funktion "sĂ„ snart som möjligt", men frĂ„gan Ă€r "vad Ă€r poĂ€ngen?". Plötsligt skickar vi ett meddelande, och ett avbrott intrĂ€ffar som förstör vĂ€rdena pĂ„ variablerna. Eller till exempel, ett nytt vĂ€rde frĂ„n servern har kommit medan vi Ă€nnu inte har skrivit det gamla. SĂ„, antingen behöver vi bygga synkronisering eller hitta ett annat sĂ€tt.

Och dÄ och dÄ fÄr jag RuntimeError: schedule stack full och vem vet varför?

Med explicit polling och uasync blir det i det hÀr fallet lite snyggare och mer pÄlitligt.

Jag har flyttat arbetet med EEPROM till en liten klass.

class EEPROM():
    i2c_addr = const(80)

    def __init__(self, i2c):
        self.i2c = i2c
        self.i2c_buf = bytearray(4) # Undvik att skapa/döda buffern vid varje anrop


    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)

I Python Àr det ganska komplicerat att arbeta direkt med byte, men det Àr ju byte som skrivs till minnet. Jag var tvungen att bygga en konvertering mellan heltal och byte med hjÀlp av biblioteket ustruct.

För att jag inte alltid ska behöva skicka I2C-objektet och minnesadressen har jag allt detta i en liten och praktisk klass.

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)

SjÀlva I2C-objektet skapas med följande parametrar.

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

Vi nĂ€rmar oss det mest intressanta — implementationen av kommunikationen med servern via MQTT. Man behöver inte implementera sjĂ€lva protokollet — det har hittats en. fĂ€rdig asynkron implementation.. Den kommer vi att anvĂ€nda.

Allt det mest intressanta samlas i klassen CounterMQTTClient, som baseras pÄ bibliotekets MQTTClient. Vi börjar med periferin.

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

HÀr skapas och konfigureras stift för lampan och knappen, samt objekt för kallt och varmt vatten.

Initiering Àr inte sÄ trivialt

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

InstÀllningarna för mqtt_as-bibliotekets funktioner anvÀnds genom en stor ordbok med olika instÀllningar - config. De flesta av instÀllningarna passar oss som standard, men mÄnga instÀllningar mÄste anges uttryckligen. För att inte behöva skriva instÀllningar direkt i koden, lagrar jag dem i en textfil som heter config.txt. Detta gör att man kan Àndra koden oberoende av instÀllningarna, samt skapa flera identiska enheter med olika parametrar.

Den sista kodblocken startar flera korutiner för att hantera olika systemfunktioner. HÀr Àr till exempel en korutin som hanterar rÀknare.

    async def _counter_coro(self, counter, topic):
        # Publicera initialvÀrdet
        value = counter.value()
        await self.publish(topic, str(value))

        # Publicera varje nytt vÀrde
        while True:
            value = await counter
            await self.publish_msg(topic, str(value))

Korutinen vÀntar i en loop pÄ ett nytt rÀknarvÀrde, och sÄ snart det dyker upp, skickar den ett meddelande via MQTT-protokollet. Den första delen av koden skickar det initiala vÀrdet Àven nÀr vattnet inte flödar genom rÀknaren.

Bas klassen MQTTClient sköter sig sjÀlv, initierar WiFi-anslutningen och Äteransluter nÀr anslutningen försvinner. NÀr WiFi-anslutningens tillstÄnd Àndras informerar biblioteket oss genom att anropa wifi_connection_handler.

    async def wifi_connection_handler(self, state):
        self.internet_outage = not state
        if state:
            self.dprint('WiFi Àr uppe.')
            duration = ticks_diff(ticks_ms(), self.internet_outage_start) // 1000
            await self.publish_debug_msg('ÅteranslutenEfter', duration)
        else:
            self.internet_outages += 1
            self.internet_outage_start = ticks_ms()
            self.dprint('WiFi Àr nere.')
            
        await asyncio.sleep(0)

Funktionen Àr Àrligt kopierad frÄn exemplen. I det hÀr fallet rÀknar den antalet avbrott (internet_outages) och deras varaktighet. Vid ÄterstÀllande av anslutningen skickas driftstoppets tid till servern.

För att nĂ€mna det, den sista sleep Ă€r bara för att göra funktionen asynkron — i biblioteket anropas den via await, annars kan endast funktioner anropas dĂ€r det finns en annan await.

Förutom att ansluta till WiFi mÄste en anslutning till MQTT-brokern (servern) ocksÄ upprÀttas. Det hÀr hanteras ocksÄ av biblioteket, och vi fÄr möjlighet att göra nÄgot anvÀndbart nÀr anslutningen Àr etablerad.

    async def mqtt_connection_handler(self, client):
        await client.subscribe(self._mqtt_cold_water_theme)
        await client.subscribe(self._mqtt_hot_water_theme)

HĂ€r prenumererar vi pĂ„ flera meddelanden — servern har nu möjlighet att ange aktuella vĂ€rden för rĂ€knarna genom att skicka motsvarande meddelande.

    def mqtt_msg_handler(self, topic, msg):
        topicstr = str(topic, 'utf8')
        self.dprint("Mottaget MQTT-meddelande 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))

Denna funktion hanterar de mottagna meddelandena, och beroende pÄ Àmnet (namnet pÄ meddelandet) uppdateras vÀrdena för en av rÀknarna.

Ett par hjÀlpfunktioner

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

Denna funktion skickar ett meddelande om anslutningen Àr etablerad. Om det inte finns nÄgon anslutning ignoreras meddelandet.

Och det hÀr Àr bara en praktisk funktion som formar och skickar debug-meddelanden.

    async def publish_debug_msg(self, subtopic, msg):
        await self.publish_msg("{}\/{}".format(self._mqtt_debug_water_theme, subtopic), str(msg))

SÄ mycket text, och vi har fortfarande inte blinkat med lysdioden. HÀr Àr det.

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

Jag har förutsett 2 blinklÀgen. Om anslutningen Àr borta (eller bara hÄller pÄ att etableras), kommer enheten att blinka snabbt. Om anslutningen Àr etablerad blinkar enheten en gÄng var 5:e sekund. Vid behov kan andra blinklÀgen implementeras hÀr.

Men lysdioden Àr bara en lek. Vi har ju ocksÄ tÀnkt pÄ displayen.

    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)

Det hĂ€r Ă€r vad jag pratade om — hur enkelt och bekvĂ€mt det Ă€r med korutin. Den hĂ€r lilla funktionen beskriver ALL interaktion med anvĂ€ndaren. Korutinen vĂ€ntar bara pĂ„ att knappen trycks och slĂ„r pĂ„ skĂ€rmen i 3 sekunder. PĂ„ skĂ€rmen visas aktuella rĂ€kneverk.

Det finns fortfarande nĂ„gra smĂ„saker kvar. HĂ€r Ă€r funktionen som (Ă„ter)startar allt detta. Huvudloopen skickar bara olika felsökningsinformation en gĂ„ng i minuten. Jag ger det som det Ă€r — det behövs nog inga sĂ€rskilda kommentarer.

   async def main(self):
        while True:
            try:
                await self._connect_to_WiFi()
                await self._run_main_loop()
                    
            except Exception as e:
                self.dprint('Global kommunikationsfel: ', e)
                await asyncio.sleep(20)

    async def _connect_to_WiFi(self):
        self.dprint('Ansluter till WiFi och 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('Ansluten!')
        self.internet_outage = False

    async def _run_main_loop(self):
        # Loopa för alltid
        mins = 0
        while True:
            gc.collect()  # För RAM-statistik.
            mem_free = gc.mem_free()
            mem_alloc = gc.mem_alloc()

            try:
                await self.publish_debug_msg("Uptid", mins)
                await self.publish_debug_msg("Repubs", self.REPUB_COUNT)
                await self.publish_debug_msg("Avbrott", 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("Ett fel intrÀffade: ", e)
            mins += 1

            await asyncio.sleep(60)

NÄgra fler instÀllningar och konstanter för att komplett beskriva.

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

Detta startas sÄ hÀr

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

NÄgot har hÀnt med mitt minne.

SĂ„, hela koden finns hĂ€r. Jag laddade upp filerna med hjĂ€lp av verktyget ampy — det lĂ„ter mig ladda upp dem till det interna (den som sitter i ESP-07) flashminnet och sedan fĂ„ tillgĂ„ng till dem frĂ„n programmet som vanliga filer. Dit laddade jag ocksĂ„ de bibliotek jag anvĂ€nder: mqtt_as, uasyncio, ssd1306 och collections (som anvĂ€nds inuti mqtt_as).

Vi startar och... fÄr MemoryError. Ju mer jag försökte förstÄ var minnet lÀcker, desto tidigare uppstod det hÀr felet. En snabb googling ledde mig till insikten att mikrokontrollern i princip har 30kB minne dÀr 65kB kod (inklusive bibliotek) definitivt inte fÄr plats.

Men det finns en lösning. Det visar sig att micropython inte kör koden direkt frĂ„n .py-filen — den hĂ€r filen kompileras först. Den kompileras faktiskt direkt pĂ„ mikrokontrollern och omvandlas till bytekod som sedan lagras i minnet. Dessutom behövs en viss mĂ€ngd RAM för att kompilatorn ska fungera.

Tricket Àr att frigöra mikrokontrollern frÄn resurskrÀvande kompilering. Man kan kompilera filer pÄ en kraftfull dator och sedan ladda den fÀrdiga bytekoden till mikrokontrollern. För detta behöver man ladda ner micropython-firmware och bygga verktyget mpy-cross.

Jag skrev inte ett Makefile, utan gick istÀllet manuellt igenom och kompilerade alla nödvÀndiga filer (inklusive bibliotek) ungefÀr sÄ hÀr

mpy-cross water_counter.py

Det ÄterstÄr bara att ladda upp filerna med .mpy-Àndelse, och kom ihÄg att ta bort de motsvarande .py-filerna frÄn enhetens filsystem först.

All utveckling utförde jag i programmet (IDE?) ESPlorer. Det lÄter oss ladda upp skript till mikrokontrollern och köra dem direkt. I mitt fall ligger hela logiken och skapandet av alla objekt i filen water_counter.py (.mpy). Men för att allt ska köras automatiskt vid start behövs Àven en fil med namnet main.py. Det mÄste vara just en .py, inte en förkompilerad .mpy. HÀr Àr dess triviala innehÄll

import water_counter

Vi kör— allt fungerar. Men det finns en oroande lite ledigt minne — cirka 1 kB. Jag har fortfarande planer pĂ„ att utöka enhetens funktionalitet, och den hĂ€r kilobyten kommer definitivt att vara för lite för mig. Men det visade sig att det finns en lösning pĂ„ det ocksĂ„.

HĂ€r Ă€r saken. Även om filerna har kompilerats till bytekod och ligger i det interna filsystemet, laddas de Ă€ndĂ„ in i RAM-minnet och körs dĂ€rifrĂ„n. Men det visar sig att micropython kan köra bytekod direkt frĂ„n flashminnet, men för detta mĂ„ste man bygga in det direkt i firmwaren. Det Ă€r inte svĂ„rt, Ă€ven om det tog lite tid pĂ„ min netbook (jag hade Linux dĂ€r).

Algoritmen Àr följande:

  • Ladda ner och installera ESP Open SDK. Den hĂ€r saken bygger kompilatorn och biblioteken för program för ESP8266. Den byggs enligt instruktionerna pĂ„ projektets startsida (jag valde installationen STANDALONE=yes)
  • Ladda ner kĂ€llkod för micropython
  • LĂ€gg de nödvĂ€ndiga biblioteken i ports/esp8266/modules i micropython-strukturen
  • Bygg firmwaren enligt instruktionerna i filen ports/esp8266/README.md
  • Laddar in firmware i mikrokontrollern (jag gör det pĂ„ Windows med program som ESP8266Flasher eller Pythons esptool).

Nu kommer 'import ssd1306' att hÀmta koden direkt frÄn firmware och inte anvÀnda RAM för detta. Med det hÀr trickset har jag laddat in endast bibliotekskoden i firmware, medan huvudkoden för programmet körs frÄn filsystemet. Detta möjliggör enkelt modifiering av programmet utan att behöva omkompilera firmware. Jag har för nÀrvarande cirka 8,5 kB RAM kvar. Detta gör att jag kan implementera mycket mer anvÀndbar funktionalitet i framtiden. Om minnet blir helt otillrÀckligt kan jag ocksÄ trycka in huvudprogrammet i firmware.

Och vad ska jag göra med detta nu?

Okej, hÄrdvaran Àr lödd, firmware Àr skriven, lÄdan Àr utskriven, enheten Àr fastsatt pÄ vÀggen och blinkar glatt med en lampa. Men hittills Àr detta fortfarande en svart lÄda (bÄde bokstavligt och bildligt) och det finns fortfarande inte sÄ mycket nytta av den. Det Àr dags att göra nÄgot med MQTT-meddelanden som skickas till servern.

Mitt "smart hem" körs pĂ„ Majordomo-systemet.. MQTT-modulen finns kanske redan frĂ„n början, eller sĂ„ kan den lĂ€tt installeras frĂ„n tillĂ€ggsmarknaden – jag minns inte riktigt varifrĂ„n jag fĂ„tt den. MQTT Ă€r inte sjĂ€lvförsörjande – en sĂ„ kallad broker behövs – en server som tar emot, sorterar och vidarebefordrar MQTT-meddelanden till klienterna. Jag anvĂ€nder mosquitto, som (liksom majordomo) körs pĂ„ samma netbook.

SÄ fort enheten skickar ett meddelande, dyker vÀrdet genast upp i listan.

Anslut vattenmÀtaren till det smarta hemmet

Dessa vĂ€rden kan nu kopplas till systemets objekt, de kan anvĂ€ndas i automatiseringsscenarier och genomgĂ„ olika analyser – allt detta ligger utanför denna artikels omfattning. För de som Ă€r intresserade av majordomo-systemet kan jag rekommendera kanalen Elektronik i Linsen – en vĂ€n som ocksĂ„ bygger ett smart hem och tydligt förklarar hur man konfigurerar systemet.

Jag kommer att visa bara nÄgra grafer. Detta Àr en enkel graf över vÀrden under en dag.

Anslut vattenmÀtaren till det smarta hemmet
Det Ă€r tydligt att ingen anvĂ€nde vatten pĂ„ natten. NĂ„gra gĂ„nger har nĂ„gon gĂ„tt pĂ„ toaletten, och det verkar som om omvĂ€nd osmosfilter drar nĂ„gra liter under natten. PĂ„ morgonen ökar förbrukningen avsevĂ€rt. Vanligtvis anvĂ€nder jag vatten frĂ„n beredaren, men hĂ€r ville jag ta ett bad och tillfĂ€lligt bytte till kommunalt varmvatten – det syns tydligt pĂ„ den nedre grafen.

FrĂ„n den hĂ€r grafen har jag lĂ€rt mig att toalettbesök motsvarar 6-7 liter vatten, att duscha – 20-30 liter, att diska ungefĂ€r 20 liter, och att ta ett bad krĂ€ver 160 liter. Under en dag förbrukar min familj ungefĂ€r 500-600 liter.

För sÀrskilt nyfikna gÄr det att kika pÄ registreringarna för varje specifikt vÀrde.

Anslut vattenmÀtaren till det smarta hemmet

HÀrifrÄn lÀrde jag mig att vatten rinner med en hastighet av ungefÀr 1 liter var 5:e sekund med öppet kran.

Men i sin nuvarande form Àr statistiken kanske inte sÄ lÀtt att titta pÄ. I majordomo finns det Àven möjlighet att se vattenförbrukning i grafer per dag, vecka och mÄnad. HÀr Àr till exempel en stapeldiagram över vattenförbrukningen.

Anslut vattenmÀtaren till det smarta hemmet

För tillfĂ€llet har jag data endast för en vecka. Om en mĂ„nad kommer den hĂ€r grafen att vara mer representativ – varje dag kommer att ha en egen stapel. VĂ€rdena som jag manuellt matar in stör tyvĂ€rr bilden lite (den största stapeln). Och det Ă€r fortfarande oklart om jag felaktigt stĂ€llt in de första vĂ€rdena nĂ€stan en kub mindre, eller om det hĂ€r Ă€r en bugg i mjukvaran och inte alla liter registrerades. Jag behöver mer tid.

Grafikerna sjĂ€lva behöver fortfarande lite arbete, kanske mĂ„la och pyssla. Jag övervĂ€ger ocksĂ„ att bygga en graf över minnesanvĂ€ndning av felsökningsskĂ€l – kanske det lĂ€cker nĂ„got dĂ€r. Jag funderar pĂ„ att pĂ„ nĂ„got sĂ€tt visa perioder nĂ€r internet var frĂ„nvarande. Just nu Ă€r allt detta pĂ„ idĂ©stadiet.

Slutsats

Idag har min lÀgenhet blivit lite smartare. Med en sÄ hÀr liten enhet kommer det att vara lÀttare för mig att övervaka vattenförbrukningen i huset. Om jag tidigare klagade pÄ "Äterigen för mycket vatten denna mÄnad", sÄ kommer jag nu kunna identifiera kÀllan till den förbrukningen.

NÄgon kanske tycker det Àr konstigt att se mÀtvÀrden pÄ skÀrmen om den Àr en meter frÄn mÀtaren. Men i en inte sÄ avlÀgsen framtid planerar jag att flytta till en annan lÀgenhet dÀr det kommer att finnas flera vattenstammar och dÀr mÀtarna förmodligen kommer att vara placerade pÄ trapphuset. SÄ en enhet för fjÀrravlÀsning av mÀtvÀrden kommer att vara mycket anvÀndbar.

Jag planerar ocksÄ att utöka enhetens funktionalitet. Jag kollar redan pÄ motoriserade ventiler. Just nu behöver jag vrida tre kranar i en svÄrÄtkomlig nisch för att vÀxla mellan boiler-stadsvatten. Det skulle vara mycket bekvÀmare att göra det med en knapp med motsvarande indikation. Och sjÀlvklart Àr det vÀrt att implementera lÀckageskydd.

I artikeln presenterar jag min version av en enhet baserad pĂ„ ESP8266. Enligt min Ă„sikt har jag lyckats skapa en ganska intressant firmware-lösning med micropython som anvĂ€nder korutiner — enkelt och snyggt. Jag har försökt att beskriva mĂ„nga aspekter och misstag som jag stötte pĂ„ under vĂ€gen. Kanske har jag beskrivit allt för detaljerat, personligen som lĂ€sare Ă€r det lĂ€ttare för mig att blĂ€ddra igenom det onödiga Ă€n att senare försöka lista ut vad som inte blev sagt.

Som alltid Àr jag öppen för konstruktiv kritik.

KĂ€llkod
Schema och kort
Modell av hölje

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster