Raspberry Pi Zero Wで傘リマインダーを作る

電子工作ど素人ですが、手始めに

* Raspberry Pi Zero W
* LCD
* 人感センサー

を使って傘リマインダーを作ります。
携帯の天気アプリで「雨が降りそうなときは朝何時に通知」というものはありますが、出かけるときには結局忘れてしまっていることがあるので。。玄関に置いておいて、人感センサーの入力をトリガーにLCDに降水確率を表示する感じです。電源バッテリー化もしたいです。場合によっては消費電力低減も検討してみる。
一応一通り完成しました！技術的に高度なことは一つもありません。

- [x] LCDの使い方
- [x] 人感センサーの使い方
- [x] 降水確率の取得
- [x] 各機能の組み合わせ
- [x] 電源バッテリー化
- [ ] 消費電力低減


# できあがりの図
こんな感じになりました。玄関で適当に支度していると、ちゃんとお知らせしてくれます！
このバッテリーでどれくらいの時間持つのか今後検証していきたいと思います。
![4AF816CA-8529-40AA-8761-2ECC9EB76447-2791-000001FB8590F7A4.jpg](https://qiita-image-store.s3.amazonaws.com/0/194352/ff8c9dfe-0a15-96f3-4ba5-9a5e342a870a.jpeg)

# 準備したもの
- [Raspberry Pi Zero W](http://www.rt-shop.jp/index.php?main_page=product_info&cPath=1334_1284_1372&products_id=3592)
- [GPIO Hammer Header](http://www.rt-shop.jp/index.php?main_page=product_info&cPath=1334_1284_1371&products_id=3566)
- [OSOYOO 電子部品セット](https://www.amazon.co.jp/%E5%88%9D%E5%BF%83%E8%80%85%E6%BC%94%E7%BF%92%E7%94%A8%E3%83%91%E3%83%BC%E3%83%84%E3%82%BB%E3%83%83%E3%83%88-%E5%9B%9E%E8%B7%AF%E9%85%8D%E7%B7%9A%E5%9B%B3%E3%81%A8%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%B9%E3%82%B1%E3%83%83%E3%83%81%E6%9C%89%E3%82%8A-%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BF%E3%83%BC%E5%AD%A6%E7%BF%92%E3%82%AD%E3%83%83%E3%83%88-%E3%83%AA%E3%83%AC%E3%83%BC%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E5%88%B6%E5%BE%A1-%E8%B5%A4%E5%A4%96%E7%B7%9A%E3%83%AA%E3%83%A2%E3%82%B3%E3%83%B3%E7%AD%89IoT%E3%82%92%E5%AE%9F%E8%B7%B5%E3%81%99%E3%82%8B%E9%9B%BB%E5%AD%90%E9%83%A8%E5%93%81%E3%82%BB%E3%83%83%E3%83%88/dp/B01M6ZFNSS)

GPIO Hammer Headerははんだ付けに自信がないので買いました。
OSOYOO 電子部品セットは、電子工作経験ゼロの人にはおすすめです。安いしチュートリアルも簡単だし満足です。

# LCDの使い方
以下を参照。
http://osoyoo.com/ja/2016/04/10/drive-16x2-lcd-with-raspberry-pi/
http://osoyoo.com/ja/2016/06/01/drive-i2c-lcd-screen-with-raspberry-pi/

# 人感センサーの使い方
以下を参照。
http://osoyoo.com/ja/2016/07/14/motionsensor-pi/

# 降水確率の取得
次の子記事を作成しました。
[PythonでXMLから降水確率を取得する](http://qiita.com/mnishz/items/d391cfae0ca01c6f1bd8)

# 各機能の組み合わせ
一通り出来上がりましたが、ここに至るまでにLCDのVCCとGNDを逆につないで一台壊してしまいました:cry:

- 改善点
    - ~~12-18hと18-24hで、requests.get()がそれぞれ一回ずつ呼ばれる。~~
        - 2017/08/13 追記: @shiracamusさんから教えていただいたタプル内包表記(厳密にはジェネレータ？)を使って修正済み。
    - バックライトが付いているのかを調べる方法が分からない。
        - bus.read_byte()をやってみたが、何故かバックライトの情報だけ見えない。

```py:umbrella_reminder.py
#!/usr/bin/env python2
# coding: utf-8

# If motion sensor detected a person, get and show today's rain fall chance
# of Yokohama (East of Kanagawa) on LCD for 30 seconds.
# If not, wait 1 second and check again.


# for general use
import time

# for motion sensor (referred to as "MS")
import RPi.GPIO as GPIO

# for LCD
import smbus

# for rain fall chance
import datetime
import requests
import xml.etree.ElementTree as ET


# constants for motion sensor

MS_GPIO_INPUT_PIN = 14


# constants for LCD

LCD_I2C_ADDR                = 0x3F
LCD_CHR_DISP_WIDTH          = 16
LCD_CHR_DRAM_WIDTH          = 0x28
LCD_NUM_LINES               = 2
LCD_PULSE_WAIT              = 0.0005
LCD_DELAY_WAIT              = 0.0005

I2C_CMD_MODE                = 0x00
I2C_CHR_MODE                = 0x01
I2C_ENABLE_BIT              = 0x04
I2C_BACKLIGHT_OFF           = 0x00
I2C_BACKLIGHT_ON            = 0x08


bus = smbus.SMBus(1)


def initialize_gpio_for_ms():

    GPIO.setwarnings(False)
    GPIO.cleanup()
    GPIO.setwarnings(True)

    GPIO.setmode(GPIO.BCM)
    GPIO.setup(MS_GPIO_INPUT_PIN, GPIO.IN, pull_up_down = GPIO.PUD_UP)


def initialize_lcd():

    send_byte_to_lcd(0x33, I2C_CMD_MODE) # establish 4 bits mode
    send_byte_to_lcd(0x32, I2C_CMD_MODE) # establish 4 bits mode

    send_byte_to_lcd(0x01, I2C_CMD_MODE) # 000001 clear display, return cursor to the home position
    # send_byte_to_lcd(0x02, I2C_CMD_MODE) # 000010 return cursor to the home position
    send_byte_to_lcd(0x06, I2C_CMD_MODE) # 000110 cursor move direction: increment, display shift: disabled
    send_byte_to_lcd(0x0C, I2C_CMD_MODE) # 001100 display on, cursor off, blink off
    # send_byte_to_lcd(0x10, I2C_CMD_MODE) # 010000 move cursor without writing, not used for initialization
    send_byte_to_lcd(0x28, I2C_CMD_MODE) # 101000 4 bits mode, use 2 lines, use 5 dots font
    time.sleep(LCD_DELAY_WAIT)


def send_byte_to_lcd(lcd_data, i2c_mode):

    first_data  = ((lcd_data & 0xF0) << 0) | i2c_mode
    second_data = ((lcd_data & 0x0F) << 4) | i2c_mode

    write_and_toggle_lcd_enable(first_data)
    write_and_toggle_lcd_enable(second_data)


def write_and_toggle_lcd_enable(data):

    time.sleep(LCD_DELAY_WAIT)
    bus.write_byte(LCD_I2C_ADDR, (data | I2C_ENABLE_BIT))
    time.sleep(LCD_PULSE_WAIT)
    bus.write_byte(LCD_I2C_ADDR, (data & ~I2C_ENABLE_BIT))
    time.sleep(LCD_DELAY_WAIT)


def turn_off_lcd():

    send_byte_to_lcd(0x08, (I2C_CMD_MODE | I2C_BACKLIGHT_OFF))


def turn_on_lcd():

    send_byte_to_lcd(0x0C, (I2C_CMD_MODE | I2C_BACKLIGHT_ON))


def move_cursor(line, offset):

    if line < 0 or LCD_NUM_LINES <= line:
        print('invalid line')
        return False

    if offset < 0 or LCD_CHR_DRAM_WIDTH <= offset:
        print('invalid offset')
        return False

    if   line == 0:
        line_data = 0x00
    elif line == 1:
        line_data = 0x40

    data = 0x80 + line_data + offset
    send_byte_to_lcd(data, (I2C_CMD_MODE | I2C_BACKLIGHT_ON))

    return True


def show_text_to_line(message, line):

    if move_cursor(line, 0) == False:
        return

    message = message.ljust(LCD_CHR_DISP_WIDTH)

    for i in range(LCD_CHR_DISP_WIDTH):
        send_byte_to_lcd(ord(message[i]), (I2C_CHR_MODE | I2C_BACKLIGHT_ON))


def get_yokohama_rain_fall_chances():

    today = datetime.datetime.today().strftime("%Y/%m/%d")

    url = 'http://www.drk7.jp/weather/xml/14.xml'
    response = requests.get(url)

    root = ET.fromstring(response.content)

    return ((period.get('hour'), period.text)
            for area in root.iter('area')
            if area.get('id').encode('utf-8') == '東部'
            for info in area.findall('info')
            if info.get('date') == today
            for period in info.find('rainfallchance').findall('period'))


def main():

    initialize_gpio_for_ms()
    initialize_lcd()

    target_periods = '12-18', '18-24'

    while True:

        motion_detected = True if GPIO.input(MS_GPIO_INPUT_PIN) != 0 else False

        if motion_detected:

            print('motion detected')

            turn_on_lcd()

            curr_line = 0
            for period, rain_fall_chance in get_yokohama_rain_fall_chances():
                if period in target_periods:
                    text = period + 'h ' + rain_fall_chance + '%'
                    show_text_to_line(text, curr_line)
                    print(text)
                    curr_line += 1
                    if curr_line >= LCD_NUM_LINES:
                        break

            time.sleep(30)

        else:

            print('motion not detected')

            turn_off_lcd()
            time.sleep(1)


if __name__ == '__main__':

    try:
        main()

    except KeyboardInterrupt:
        pass

    finally:
        send_byte_to_lcd(0x01, I2C_CMD_MODE)
        turn_on_lcd()
        show_text_to_line('Goodbye!', 0)
        GPIO.cleanup()
```

# 電源バッテリー化
いろいろ見ていると「最近のモバイルバッテリーは微電流の場合に保護回路が働いて出力が止まる」というようなことが書いてあって心配していましたが、普通に手持ちのバッテリーが使えました。
安物だから保護回路が動いていないのかしら？それともそこそこ電流が流れている？消費電力の測定もしてみたいです。
[ANKER Astro Slim2](https://www.amazon.co.jp/gp/product/B00CTK762E/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1)

# 消費電力低減
まだやってない。