ラズパイとLCDディスプレイでお天気ディスプレイを作る

# 完成品

これです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102797/60acb75c-ce86-7846-3839-e0fb5eba68b6.png)

気象庁から天気予報データを取得して
今日、明日の天気、最高気温、最低気温、降水確率
を表示します

# 古いラズパイを久々に起動したら...

今回使った Raspberry Pi の OS は jessie
古いバージョンなので、サポートが終了しています

``` $ sudo apt-get update ```
を実行したところ、aptの参照先が無くなっていてエラー発生

```/etc/apt/sources.list``` を書き換えて対応しました

```bash:/etc/apt/sources.list
pi@raspberrypi:~ $ sudo cat /etc/apt/sources.list
deb http://legacy.raspbian.org/raspbian/ jessie main contrib non-free rpi
```

# LCDをつなげる

## 買ったもの

ラズパイから制御できる LCDキャラクタディスプレイモジュール を買いました
以下の条件で探してAmazonで購入

* 20文字 x 4行
* I2CシリアルIFモジュールつき
* バックライトつき

949円でした

## 結線

ジャンパワイヤーで、以下のように結線します

| I2CシリアルIFモジュール | Raspberry Pi |
| :---: | :---: |
| GND | PIN6 (GND) |
| VCC | PIN4 (5V) |
| SDA | PIN3 (GPIO2) |
| SCL | PIN5 (GPIO3) |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102797/731bf741-63c6-5320-706f-8bf1bbe92d9c.png)

## ドライバ、ライブラリ、テストコード

[TheRaspberryPiGuy氏のLCD制御プログラム](https://github.com/the-raspberry-pi-guy/lcd/blob/master/README.md)を利用しました

```bash
$ git clone https://github.com/the-raspberry-pi-gui/lcd.git
$ cd lcd/
$ sudo sh install.sh

$ python demo_clock.py
```

無事ディスプレイに時計が表示されました
I2CシリアルIFモジュール と TheRaspberryPiGuy氏 優秀すぎ
試行錯誤を覚悟していたのですが、電子工作もプログラミングも不要でした

## カタカナの表示

[Extended Strings](https://github.com/the-raspberry-pi-guy/lcd/blob/master/README.md#extended-strings) を利用すると半角カナが表示できます

UTF-8のコードと比較して、簡単な変換関数を作りました

> UTF-8 : 半角カナ
> https://orange-factory.com/sample/utf8/code3/ef.html#HalfwidthandFullwidthForms
> ヲ : 0xEF 0xBD **0xA6**
> タ : 0xEF 0xBE **0x80**
>
> lcd : Extended string code
> https://github.com/the-raspberry-pi-guy/lcd/blob/master/README.md#extended-strings
> ヲ : **0xA6** ( + 0x00 )
> タ : **0xC0** ( + 0x40 )

```python
def convert_to_code(input_string):
    char_codes = []
    for char in input_string:
        c = ord(char) & 0xFF
        c += 0x40 if c < 0xA0 else 0
        char_code = "{{0x{:02X}}}".format(c)
        char_codes.append(char_code)
    return "".join(char_codes[2::3])
```
ヒヤヒヤするコードですね
ロバスト性皆無なので、絶対に半角カナ以外入れちゃだめだぞ～

```demo_clock.py```を改造して、こんな感じ (↓) でカタカナの表示に成功しました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102797/94ee12ee-9762-5f13-5b5b-be72996790a0.png)


# 天気予報の取得

## 気象庁のURLとjson

天気予報は気象庁が提供するjsonを利用します
このようなURLからjsonがとれます。素晴らしいです
https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json

jsonの例です

```json
[
	{
		"publishingOffice": "気象庁",
		"reportDatetime": "2023-07-26T17:00:00+09:00",
		"timeSeries": [
			{
				"timeDefines": [
					"2023-07-26T17:00:00+09:00",
					"2023-07-27T00:00:00+09:00",
					"2023-07-28T00:00:00+09:00"
				],
				"areas": [
					{
						"area": {
							"name": "東京地方",
							"code": "130010"
						},
						"weatherCodes": [
							"100",
							"100",
							"100"
						],
						"weathers": [
							"晴れ",
							"晴れ　多摩西部　では　昼過ぎ　から　雨　で　雷を伴う",
							"晴れ"
						],
```

```weatherCodes``` と ```weathers``` に天気情報が入っています
```weathers``` は日本語かつ長い(ときもある)ので、```weatherCodes``` を使います

気象庁のjsonについては以下のblogなどを参考にしました

https://anko.education/webapi/jma

## 天気コードと "読み" への変換

```weatherCodes```は天気に対応していて、例えば **100** は **晴れ** です
対応表は、 ```Forcast.Const.TELOPS``` を気象庁の天気予報のページから入手して
手元でそれを変換して使いました

詳しくはこちらの記事を参照のこと

https://qiita.com/Tomonobu3110/items/50c3a7c02d4b4cf6f013

### 手順

1. ```Forcast.Const.TELOPS``` をローカルにファイル保存
1. 読みを付加
1. 全角カタカナを半角カタカナに置換 (by [秀丸エディタ](https://hide.maruo.co.jp/software/hidemaru.html))
1. ```TELOPS_hankaku_kana.csv``` というファイル名で保存

# プログラム・コード

お天気ディスプレイのコードです

```python:demo_tenki.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Weather information program.
# Demo program for the I2C 20x4 Display from Ryanteck.uk
# Created by https://github.com/Tomonobu3110

# Import necessary libraries for communication and display use

## for weather data
import requests
import json
import csv
import unicodedata

## for LCD display
import drivers
from time import sleep
from datetime import datetime

# Convart charactor code
# from Hankaku Kana (UTF-8) to extended string code
# utf-8           : https://orange-factory.com/sample/utf8/code3/ef.html#HalfwidthandFullwidthForms
# extended string : https://github.com/the-raspberry-pi-guy/lcd/blob/master/README.md#extended-strings
def convert_to_code(input_string):
    char_codes = []
    
    for char in input_string:
        c = ord(char) & 0xFF
        c += 0x40 if c < 0xA0 else 0
        char_code = "{{0x{:02X}}}".format(c)
        char_codes.append(char_code)
    
    return "".join(char_codes[2::3]) ## Python syntax : array[start:stop:step]

# Get json from specified URL
def get_json_from_url(url):
    try:
        response = requests.get(url)
        response.raise_for_status()  # error check
        return response.json()
    except requests.exceptions.RequestException as e:
        print("Error during request: {}".format(e))
        return None
    except json.JSONDecodeError as e:
        print("Error parsing JSON: {}".format(e))
        return None

# open TELOPS dictionary
def read_csv_dict(csv_file):
    data_dict = {}  # 空の辞書を作成
    
    with open(csv_file, 'rb') as file:
        csv_reader = csv.reader(file)  # ファイルハンドルからreaderオブジェクトを作成
        
        for row in csv_reader:  # 各行に対してループ
            key = int(row[0])  # 各行の最初の数をキーとして取得
            data_dict[key] = row[1:]  # キーと値を辞書に追加
            
    return data_dict

def get_date_index(json, datetime):
    if datetime[0:10] == json["today"]["date"][0:10]:
        return "today"
    if datetime[0:10] == json["tomorrow"]["date"][0:10]:
        return "tomorrow"
    return "undef"

def get_time_index(datetime):
    return int(datetime[11:13]) / 6

def get_minmax_index(datetime):
    if datetime[11:13] == "00":
        return "min"
    if datetime[11:13] == "09":
        return "max"
    return "undef"

def get_disp_str(value):
    if -1 == value:
        return "-"
    return str(value)

# Load the driver and set it to "display"
# If you use something from the driver library use the "display." prefix first
display = drivers.Lcd()

yomi_dict = read_csv_dict("TELOPS_hankaku_kana.csv")

# display weather information
try:
    while True:
        # get weather information
        url = "https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"
        json_data = get_json_from_url(url)
        
        if json_data:
            try:
                # (1) Weather
                weather_times = json_data[0]["timeSeries"][0]["timeDefines"]
                weather_areas = json_data[0]["timeSeries"][0]["areas"]
                for area in weather_areas:
                    if int(area["area"]["code"]) == 130010: # 東京地方 130010
                        weather_codes = area["weatherCodes"]
                        dates = [s[8:10] for s in weather_times]
                        weather = [convert_to_code(yomi_dict[int(c)][1]) for c in weather_codes]
                
                # (2) Rainy percent
                rainy_times = json_data[0]["timeSeries"][1]["timeDefines"]
                rainy_areas = json_data[0]["timeSeries"][1]["areas"]
                for area in rainy_areas:
                    if int(area["area"]["code"]) == 130010: # 東京地方 130010
                        rainy_pops = area["pops"]
                
                # (3) Temperature
                temp_times = json_data[0]["timeSeries"][2]["timeDefines"]
                temp_areas = json_data[0]["timeSeries"][2]["areas"]
                for area in temp_areas:
                    if int(area["area"]["code"]) == 44132: # 東京 44132
                        temp_temps = area["temps"]
                
            except KeyError:
                print("The specified path does not exist in the JSON data.")
        
        # template of weather data
        weather_data = '''\
        {
            "today": {
                "date": "2023-01-01",
                "code": 0,
                "pops": [
                    -1,
                    -1,
                    -1,
                    -1
                ],
                "temps": {
                    "min": -1,
                    "max": -1
                }
            },
            "tomorrow": {
                "date": "2023-01-02",
                "code": 0,
                "pops": [
                    -1,
                    -1,
                    -1,
                    -1
                ],
                "temps": {
                    "min": -1,
                    "max": -1
                }
            }
        }
        '''
        
        # conver to intermidiate data format
        weather_json = json.loads(weather_data)
        weather_json["today"]["date"]    = weather_times[0][0:10]
        weather_json["today"]["code"]    = int(weather_codes[0])
        weather_json["tomorrow"]["date"] = weather_times[1][0:10]
        weather_json["tomorrow"]["code"] = int(weather_codes[1])
        for i, dt in enumerate(rainy_times):
            weather_json[get_date_index(weather_json, dt)]["pops"][get_time_index(dt)] = int(rainy_pops[i])
        for i, dt in enumerate(temp_times):
            weather_json[get_date_index(weather_json, dt)]["temps"][get_minmax_index(dt)] = int(temp_temps[i])
        if weather_json["today"]["temps"]["min"] == weather_json["today"]["temps"]["max"]:
          weather_json["today"]["temps"]["min"] = -1
        #print(weather_json)
        
        print("Writing to display")
        
        display.lcd_display_extended_string(weather_json["today"]   ["date"][8:10] + convert_to_code("ﾆﾁ") + " " + convert_to_code(yomi_dict[weather_json["today"]   ["code"]][1]), 1)
        display.lcd_display_extended_string(weather_json["tomorrow"]["date"][8:10] + convert_to_code("ﾆﾁ") + " " + convert_to_code(yomi_dict[weather_json["tomorrow"]["code"]][1]), 2)
        
        temp_str = convert_to_code("ｷｵﾝ") + "  "
        temp_str += get_disp_str(weather_json["today"]["temps"]["min"]) + " "
        temp_str += get_disp_str(weather_json["today"]["temps"]["max"]) + " | "
        temp_str += get_disp_str(weather_json["tomorrow"]["temps"]["min"]) + " "
        temp_str += get_disp_str(weather_json["tomorrow"]["temps"]["max"]) + " "
        display.lcd_display_extended_string(temp_str, 3)
        
        pops_str = convert_to_code("ｱﾒ") + "   "
        pops_str += get_disp_str(max(weather_json["today"]["pops"])) + "% | "
        pops_str += get_disp_str(max(weather_json["tomorrow"]["pops"])) + "%"
        display.lcd_display_extended_string(pops_str, 4)
        
        # wait 5 min
        sleep(5 * 60)
        display.lcd_clear()
except KeyboardInterrupt:
    # If there is a KeyboardInterrupt (when you press ctrl+c), exit the program and cleanup
    print("Cleaning up!")
    display.lcd_clear()
```
中間形式のjsonのフォーマットをもうちょっと賢く作ればよかったかな...
ほかにも突っ込みどころ満載ですが、とりあえず動いているのでよしとします

# 自動起動の設定

ラズパイ起動時に自動的にスクリプトを動作させます

ほぼ、公式の```README.md```通り
https://github.com/the-raspberry-pi-guy/lcd/blob/master/README.md#systemd

一点だけ ```WorkingDirectory``` を足しました

serviceファイルの作成
```bash
$ sudo nano /lib/systemd/system/rpi-lcd.service
```

serviceファイルの内容
```ini:rpi-lcd.service
[Unit]
Description=RPi Python script for a 16x2 LCD

[Service]
Type=simple
## Edit the following according to the script permissions
User=pi
#Group=users

## Working directory
WorkingDirectory=/home/pi/lcd

## Edit the following with the full path to the compatible Python version and your script
ExecStart=/usr/bin/python /home/pi/lcd/demo_tenki.py

Restart=always
RestartSec=5

KillMode=process
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target
```

サービスの有効化
```bash
$ sudo systemctl enable rpi-lcd.service
$ sudo systemctl start rpi-lcd.service
```


完成品を再掲！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102797/60acb75c-ce86-7846-3839-e0fb5eba68b6.png)

以上です