Mumbling about computers

IOT house with Sonoff and MicroPython

2018-08-06 [ iot ]

Idea

I bought a few sonoff basics and wanted to set up some sensors and triggers around the house, with some restrictions:

  • If a sensor or switch replaces something existing, then there should be always a fallback to that functionality even if there's no connectivity.
  • Code should be fully open
  • There should be no complex infrastructure to support this

Evaluating the board

I wanted a cheap board that included a mains transformer as both using phone chargers (NodeMCU) or building your own ends up being more expensive, cumbersome and prone to failures. Winner: Sonoff basic.

GPIO NAME IN/OUT NOTES
0 button IN
1 TX INOUT outputs garbage on boot
3 RX INOUT
12 relay OUT powered from mains
13 led OUT inverted high/low
14 GPIO INOUT

Also note that if you fuck up the TX/RX pins you won't be able to flash the Sonoff again.

I ended up choosing to go for independent modules written in MicroPython, where the communication bus is MQTT.

To get MicroPython on the board you should solder the 4 UART pins on the board and flash the firmware.

Flashing

Hardware setup

From easiest to hardest:

  • USB UART/TTL breakout pin
  • Arduino as a serial 'proxy'
  • Raspberry Pi GPIO

sonoff pi pinout All wired up

Flashing MicroPython

I was running (as recommended here)

esptool.py --port /dev/ttyUSB0 erase_flash
esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash --flash_size=detect 0 esp8266-20170108-v1.8.7.bin

The first issue I had was that the TX/RX pair is reversed on my revision of the board, but that's a simple cable switch. The second issue is that my firmware flashes kept failing, and as found here, the manufacturer of my board was 5e so I modified the flash command slightly to

esptool.py --port /dev/ttyUSB0 erase_flash
esptool.py --port /dev/ttyUSB0 write_flash -fs 1MB -fm dout 0x0 esp8266-20180511-v1.9.4.bin

Getting a REPL

While you can use screen, minicom, picocom or other software for serial communication, you also will (most likely) want to push files into the VFS.

What I used for both getting a REPL and pushing files is mpfshell, which sometimes sadly fails, but >90% of the time works great.

My rapid iterations are done by running

mpfshell ttyUSB0 -c "put main.py; repl; exit"

and pressing Ctrl-D immediately which soft-reboots the esp8266. I'll see my code run, play around in the REPL and iterate. To exit mpfshell you need to press Ctrl-Alt-].

Writing some code

With the way I set up the common functions, you can have something working in ~20 lines of code (omitting imports)

DHT22 -> MQTT

CLIENT_ID = 'TEMPSENSOR'
TEMPTOPIC = b"TEMP/%s" % CLIENT_ID
HUMTOPIC = b"HUM/%s" % CLIENT_ID

led = Pin(13, Pin.OUT)
dht = dht.DHT22(Pin(14))

@common.debounce(60000)
def read_dht():
    dht.measure()
    common.mqtt.publish(TEMPTOPIC, "%.2f" % dht.temperature())
    common.mqtt.publish(HUMTOPIC, "%.2f" % dht.humidity())

def main():
    setup_fns = [ lambda: led(1) # Turn off LED, it is inverted
                ]
    common.loop(CLIENT_ID, setup_fn=setup_fns, loop_fn=[read_dht], callback=sub_cb, subtopic=SUBTOPIC)

main()

Pub/sub + button for relay

CLIENT_ID = 'NIGHTLAMP'
SUBTOPIC = b"%s/set" % CLIENT_ID
PUBTOPIC = b"%s/state" % CLIENT_ID

button = Pin(0, Pin.IN)
led = Pin(13, Pin.OUT)
relay = Pin(12, Pin.OUT)

def set_pin(pin, state):
    pin(state)
    common.mqtt.publish(PUBTOPIC, str(pin()))

def sub_cb(topic, msg):
    if msg == b'1':
        set_pin(relay, True)
    elif msg == b'0':
        set_pin(relay, False)
    else:
        set_pin(relay, not relay())

@common.debounce(250)
def handle_button(pin):
    set_pin(relay, not relay())

def main():
    setup_fns = [ lambda: button.irq(handler=handle_button, trigger=Pin.IRQ_RISING),
                  lambda: led(1) # Turn off LED, it is inverted
                ]
    common.loop(CLIENT_ID, setup_fn=setup_fns, loop_fn=[], callback=sub_cb, subtopic=SUBTOPIC)

main()

The mqtt lib is umqtt.simple which is an official lib. You can find the common lib on github.

Getting code on the device

After writing the code, to put it into the device simply run

$ mpfshell ttyUSB0

** Micropython File Shell v0.9.1, sw@kaltpost.de **
-- Running on Python 3.7 using PySerial 3.4 --

mpfs [/]> put HOSTNAME
mpfs [/]> put common.py
mpfs [/]> put main.py
mpfs [/]> put mqtt.py
mpfs [/]> ls

Remote files in '/':

       HOSTNAME
       common.py
       main.py
       mqtt.py

Testing the changes:

mpfs [/]> repl
>
MicroPython v1.9.4-8-ga9a3caad0 on 2018-05-11; ESP module with ESP8266
Type "help()"
*** Exit REPL with Ctrl+] ***
for more information.
>>>
PYB: soft reboot
#6 ets_task(40100130, 3, 3fff83ec, 4)
Connected to SSID!
Subscribing to b'PRINTER_POWER/set'
Subscribing to b'PRINTER_POWER/OTA'
#> Manually pressed button
Button was pressed
Setting pin to True
Publish 1 to b'PRINTER_POWER/state'
#> Manually pressed button
Button was pressed
Setting pin to False
Publish 0 to b'PRINTER_POWER/state'
#> Send '1' via MQTT
b'PRINTER_POWER/set'
b'1'
Setting pin to True
Publish 1 to b'PRINTER_POWER/state'

Done.