以前に調べた内容を備忘録としてまとめておきます。
-
気象庁 東京都千代田区の天気予報:https://www.jma.go.jp/bosai/forecast/#area_type=class20s&area_code=1310100
-
東京都の天気予報JSON(UTF-8):https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json
areaコード
- 詳細は↓こちら(UTF-8)
weatherコード と SVG
SVGから変換したPNGたち
週間天気予報表示器
Raspberry Pi Zezo
に 640x400 のディスプレイを接続し、一時間ごとに 気象庁のサイトから 住居地域の天気予報JSONを取得して、それをディスプレイに表示しています。
- 昼モード
- 夜モード
コード
天気予報JSONから上記の画像を作成するPythonコード
import os
import locale
import datetime
import textwrap
from enum import Enum
from PIL import Image, ImageDraw, ImageFont, ImageFilter
project_root = os.getcwd()
canvasSize = (640, 400)
saturation = 1.0
black = '#000'
white = '#fff'
blue = '#00f'
red = '#f00'
yellow = '#ff0'
background = white
fontColor = black
minColor = blue
borderColor = white
locale.setlocale(locale.LC_TIME, 'ja_JP.UTF-8')
lastUpdate = 0
previous = True
weatherDescription = {
'100':'晴',
'101':'晴時々曇',
'102':'晴一時雨',
'103':'晴時々雨',
'104':'晴一時雪',
'105':'晴時々雪',
'106':'晴一時雨か雪',
'107':'晴時々雨か雪',
'108':'晴一時雨か雷雨',
'110':'晴後時々曇',
'111':'晴後曇',
'112':'晴後一時雨',
'113':'晴後時々雨',
'114':'晴後雨',
'115':'晴後一時雪',
'116':'晴後時々雪',
'117':'晴後雪',
'118':'晴後雨か雪',
'119':'晴後雨か雷雨',
'120':'晴朝夕一時雨',
'121':'晴朝の内一時雨',
'122':'晴夕方一時雨',
'123':'晴山沿い雷雨',
'124':'晴山沿い雪',
'125':'晴午後は雷雨',
'126':'晴昼頃から雨',
'127':'晴夕方から雨',
'128':'晴夜は雨',
'130':'朝の内霧後晴',
'131':'晴明け方霧',
'132':'晴朝夕曇',
'140':'晴時々雨で雷を伴う',
'160':'晴一時雪か雨',
'170':'晴時々雪か雨',
'181':'晴後雪か雨',
'200':'曇',
'201':'曇時々晴',
'202':'曇一時雨',
'203':'曇時々雨',
'204':'曇一時雪',
'205':'曇時々雪',
'206':'曇一時雨か雪',
'207':'曇時々雨か雪',
'208':'曇一時雨か雷雨',
'209':'霧',
'210':'曇後時々晴',
'211':'曇後晴',
'212':'曇後一時雨',
'213':'曇後時々雨',
'214':'曇後雨',
'215':'曇後一時雪',
'216':'曇後時々雪',
'217':'曇後雪',
'218':'曇後雨か雪',
'219':'曇後雨か雷雨',
'220':'曇朝夕一時雨',
'221':'曇朝の内一時雨',
'222':'曇夕方一時雨',
'223':'曇日中時々晴',
'224':'曇昼頃から雨',
'225':'曇夕方から雨',
'226':'曇夜は雨',
'228':'曇昼頃から雪',
'229':'曇夕方から雪',
'230':'曇夜は雪',
'231':'曇海上海岸は霧か霧雨',
'240':'曇時々雨で雷を伴う',
'250':'曇時々雪で雷を伴う',
'260':'曇一時雪か雨',
'270':'曇時々雪か雨',
'281':'曇後雪か雨',
'300':'雨',
'301':'雨時々晴',
'302':'雨時々止む',
'303':'雨時々雪',
'304':'雨か雪',
'306':'大雨',
'308':'雨で暴風を伴う',
'309':'雨一時雪',
'311':'雨後晴',
'313':'雨後曇',
'314':'雨後時々雪',
'315':'雨後雪',
'316':'雨か雪後晴',
'317':'雨か雪後曇',
'320':'朝の内雨後晴',
'321':'朝の内雨後曇',
'322':'雨朝晩一時雪',
'323':'雨昼頃から晴',
'324':'雨夕方から晴',
'325':'雨夜は晴',
'326':'雨夕方から雪',
'327':'雨夜は雪',
'328':'雨一時強く降る',
'329':'雨一時みぞれ',
'340':'雪か雨',
'350':'雨で雷を伴う',
'361':'雪か雨後晴',
'371':'雪か雨後曇',
'400':'雪',
'401':'雪時々晴',
'402':'雪時々止む',
'403':'雪時々雨',
'405':'大雪',
'406':'風雪強い',
'407':'暴風雪',
'409':'雪一時雨',
'411':'雪後晴',
'413':'雪後曇',
'414':'雪後雨',
'420':'朝の内雪後晴',
'421':'朝の内雪後曇',
'422':'雪昼頃から雨',
'423':'雪夕方から雨',
'425':'雪一時強く降る',
'426':'雪後みぞれ',
'427':'雪一時みぞれ',
'450':'雪で雷を伴う'
}
weatherIconMap = {
'100':['100', '500'],
'101':['101', '501'],
'102':['102', '502'],
'103':['102', '502'],
'104':['104', '504'],
'105':['104', '504'],
'106':['102', '502'],
'107':['102', '502'],
'108':['102', '502'],
'110':['110', '510'],
'111':['110', '510'],
'112':['112', '512'],
'113':['112', '512'],
'114':['112', '512'],
'115':['115', '515'],
'116':['115', '515'],
'117':['115', '515'],
'118':['112', '512'],
'119':['112', '512'],
'120':['102', '502'],
'121':['102', '502'],
'122':['112', '512'],
'123':['100', '500'],
'124':['100', '500'],
'125':['112', '512'],
'126':['112', '512'],
'127':['112', '512'],
'128':['112', '512'],
'130':['100', '500'],
'131':['100', '500'],
'132':['101', '501'],
'140':['102', '502'],
'160':['104', '504'],
'170':['104', '504'],
'181':['115', '515'],
'200':['200', '200'],
'201':['201', '601'],
'202':['202', '202'],
'203':['202', '202'],
'204':['204', '204'],
'205':['204', '204'],
'206':['202', '202'],
'207':['202', '202'],
'208':['202', '202'],
'209':['200', '200'],
'210':['210', '610'],
'211':['210', '610'],
'212':['212', '212'],
'213':['212', '212'],
'214':['212', '212'],
'215':['215', '215'],
'216':['215', '215'],
'217':['215', '215'],
'218':['212', '212'],
'219':['212', '212'],
'220':['202', '202'],
'221':['202', '202'],
'222':['212', '212'],
'223':['201', '601'],
'224':['212', '212'],
'225':['212', '212'],
'226':['212', '212'],
'228':['215', '215'],
'229':['215', '215'],
'230':['215', '215'],
'231':['200', '200'],
'240':['202', '202'],
'250':['204', '204'],
'260':['204', '204'],
'270':['204', '204'],
'281':['215', '215'],
'300':['300', '300'],
'301':['301', '701'],
'302':['302', '302'],
'303':['303', '303'],
'304':['300', '300'],
'306':['300', '300'],
'308':['308', '308'],
'309':['303', '303'],
'311':['311', '711'],
'313':['313', '313'],
'314':['314', '314'],
'315':['314', '314'],
'316':['311', '711'],
'317':['313', '313'],
'320':['311', '711'],
'321':['313', '313'],
'322':['303', '303'],
'323':['311', '711'],
'324':['311', '711'],
'325':['311', '711'],
'326':['314', '314'],
'327':['314', '314'],
'328':['300', '300'],
'329':['300', '300'],
'340':['400', '400'],
'350':['300', '300'],
'361':['411', '811'],
'371':['413', '413'],
'400':['400', '400'],
'401':['401', '801'],
'402':['402', '402'],
'403':['403', '403'],
'405':['400', '400'],
'406':['406', '406'],
'407':['406', '406'],
'409':['403', '403'],
'411':['411', '811'],
'413':['413', '413'],
'414':['414', '414'],
'420':['411', '811'],
'421':['413', '413'],
'422':['414', '414'],
'423':['414', '414'],
'425':['400', '400'],
'426':['400', '400'],
'427':['400', '400'],
'450':['400', '400']
}
areaCode = -1
class weatherInfomation(object):
def __init__(self, office, area):
self.office = office
self.area = area
self.jma_url = "https://www.jma.go.jp/bosai/forecast/data/forecast/{}.json".format(office)
self.loadWeatherData()
def loadWeatherData(self):
import requests
get = requests.get(self.jma_url)
get.raise_for_status()
self.weatherInfo = get.json()
area = self.weatherInfo[0]["timeSeries"][0]["areas"]
for a in range(len(area)):
if area[a]["area"]["code"] == self.area:
global areaCode
areaCode = a
break
class fonts(Enum):
thin = "./fonts/Roboto-Thin.ttf"
light = "./fonts/Roboto-Light.ttf"
normal = "./fonts/Roboto-Black.ttf"
icon = "./fonts/weathericons-regular-webfont.ttf"
nihongo = "./fonts/HiraKakuProN-W1-AlphaNum-02.otf"
#nihongo = "./fonts/x12y16pxMaruMonica.ttf"
def getFont(type, fontsize=12):
return ImageFont.truetype(type.value, fontsize)
def getDisplayColor(color):
return tuple(color_palette[color])
def poppp(p):
return p if p == "-" else p + "%"
def weather2(day, wi):
width, height = 320, 240
cv = Image.new("RGB", (width, height), background)
draw = ImageDraw.Draw(cv)
weatherCode = wi.weatherInfo[0]["timeSeries"][0]["areas"][areaCode]["weatherCodes"][day]
weatherDate = wi.weatherInfo[0]["timeSeries"][0]["timeDefines"][day]
weathers = wi.weatherInfo[0]["timeSeries"][0]["areas"][areaCode]["weathers"][day] .replace(' ', '')
winds = wi.weatherInfo[0]["timeSeries"][0]["areas"][areaCode]["winds"][day] .replace(' ', '')
if day == 0:
if len(wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"]) == 4:
tempMin = wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"][0]
tempMax = wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"][1]
else:
tempMin = "-"
tempMax = "-"
else:
if len(wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"]) == 4:
tempMin = wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"][2]
tempMax = wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"][3]
else:
tempMin = wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"][0]
tempMax = wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"][1]
omitteTemp = tempMin == '-' and tempMax == '-'
pngOffset = 0 if omitteTemp else 40
dt = datetime.datetime.fromisoformat(weatherDate)
dt_str = "{}/{}({})".format(dt.date().month, dt.date().day, dt.strftime('%a'))
pngFilename = "./images/" + weatherIconMap[weatherCode][0] + ".svg.png"
if dt.time().hour >= 12:
pngFilename = "./images/" + weatherIconMap[weatherCode][1] + ".svg.png"
weekday = dt.date().weekday()
colorWeekday = red if weekday == 6 else minColor if weekday == 5 else fontColor
nihongoFont = getFont(fonts.nihongo, fontsize=32)
textsize = draw.textsize(dt_str, font=nihongoFont)
draw.text(((width - textsize[0]) // 2, 0), dt_str, colorWeekday, font=nihongoFont)
y = textsize[1] - 16
png = Image.open(pngFilename).copy()
png = png.resize((png.width * 2, png.height * 2))
position = ((cv.width - png.width) // 2 - pngOffset, y)
cv.paste(png, position, png)
pngCenter = (position[0] + png.width, png.height // 2 + y)
#temps
if not omitteTemp:
shaFont = getFont(fonts.light, fontsize=50)
textsize = draw.textsize("\\", font=shaFont)
draw.text((pngCenter[0] + (80 - textsize[0]) // 2, pngCenter[1] - textsize[1] // 2), "\\", fontColor, font=shaFont)
tempFont = getFont(fonts.light, fontsize=32)
textsize = draw.textsize(tempMin, font=tempFont)
draw.text((pngCenter[0] + (80 // 2) - textsize[0] - 2, pngCenter[1] - 0), tempMin, minColor, font=tempFont)
textsize = draw.textsize(tempMax, font=tempFont)
draw.text((pngCenter[0] + (80 // 2) + 4, pngCenter[1] - 24), tempMax, red, font=tempFont)
#pops
y += png.height
popsArray = ["-", "-", "-", "-"]
for n in range(len(wi.weatherInfo[0]["timeSeries"][1]["timeDefines"])):
d = wi.weatherInfo[0]["timeSeries"][1]["timeDefines"][n]
dt = datetime.datetime.fromisoformat(d)
if dt.date().weekday() != weekday: continue
pp = dt.time().hour // 6
popsArray[pp] = poppp(wi.weatherInfo[0]["timeSeries"][1]["areas"][areaCode]["pops"][n])
popsStr = "{} | {} | {} | {}".format(popsArray[0], popsArray[1], popsArray[2], popsArray[3])
popFont = getFont(fonts.light, fontsize=26)
textsize = draw.textsize(popsStr, font=popFont)
draw.text(((width - textsize[0]) // 2, y), popsStr, fontColor, font=popFont)
#weathers
wrap_list = textwrap.wrap(weathers, 15)
nihongoFont = getFont(fonts.nihongo, fontsize=20)
y += textsize[1] + 8
for line in wrap_list:
textsize = draw.textsize(line, font=nihongoFont)
draw.text(((width - textsize[0]) // 2, y), line, fontColor, font=nihongoFont)
y += textsize[1]
#winds
winds = "(" + winds + ")"
nihongoFont = getFont(fonts.nihongo, fontsize=14)
textsize = draw.textsize(winds, font=nihongoFont)
draw.text(((width - textsize[0]) // 2, y), winds, minColor, font=nihongoFont)
return cv
def weather6(day, wi):
width, height = 105, 160
cv = Image.new("RGB", (width, height), background)
draw = ImageDraw.Draw(cv)
weatherCode = wi.weatherInfo[1]["timeSeries"][0]["areas"][0]["weatherCodes"][day]
weatherDate = wi.weatherInfo[1]["timeSeries"][0]["timeDefines"][day]
dt = datetime.datetime.fromisoformat(weatherDate)
dt_str = "{}({})".format(dt.date().day, dt.strftime('%a'))
weekday = dt.date().weekday()
colorWeekday = red if weekday == 6 else minColor if weekday == 5 else fontColor
nihongoFont = getFont(fonts.nihongo, fontsize=24)
textsize = draw.textsize(dt_str, font=nihongoFont)
draw.text(((width - textsize[0]) // 2, 26 - textsize[1]), dt_str, colorWeekday, font=nihongoFont)
y = textsize[1]
pngFilename = "./images/" + weatherIconMap[weatherCode][0] + ".svg.png"
png = Image.open(pngFilename).copy()
postion = ((cv.width - png.width) // 2, y)
cv.paste(png, postion, png)
desc = weatherDescription[weatherCode]
fontsize = 20
while True:
nihongoFont = getFont(fonts.nihongo, fontsize=fontsize)
textsize = draw.textsize(desc, font=nihongoFont)
if textsize[1] <= width:
break
fontsize -= 1
y += png.height
draw.text(((width - textsize[0]) // 2, y), desc, fontColor, font=nihongoFont)
pos = -1
pops = wi.weatherInfo[1]["timeSeries"][0]["areas"][0]["pops"][day]
if pops == "":
for n in range(len(wi.weatherInfo[0]["timeSeries"][1]["timeDefines"]) - 1, -1, -1):
d1 = wi.weatherInfo[0]["timeSeries"][1]["timeDefines"][n]
if weatherDate <= d1:
pos = n
pops = wi.weatherInfo[0]["timeSeries"][1]["areas"][areaCode]["pops"][n]
break
if pops == "":
print("weather6: BUG!! day: {}".format(day))
exit()
y += textsize[1] + 2
popFont = getFont(fonts.light, fontsize=20)
pops = pops + "%"
textsize = draw.textsize(pops, font=popFont)
draw.text(((width - textsize[0]) // 2, y), pops, fontColor, font=popFont)
y += textsize[1] + 2
shaFont = getFont(fonts.light, fontsize=30)
textsize = draw.textsize("\\", font=shaFont)
draw.text(((width - textsize[0]) // 2, y), "\\", fontColor, font=shaFont)
if pos == -1:
tempMin = wi.weatherInfo[1]["timeSeries"][1]["areas"][0]["tempsMin"][day]
tempMax = wi.weatherInfo[1]["timeSeries"][1]["areas"][0]["tempsMax"][day]
else:
temps = wi.weatherInfo[0]["timeSeries"][2]["areas"][areaCode]["temps"]
if len(temps) == 2:
tempMin = temps[0]
tempMax = temps[1]
else:
tempMin = temps[2]
tempMax = temps[3]
textsize = draw.textsize(tempMin, font=popFont)
draw.text(((width // 2) - textsize[0] - 4, y + 10), tempMin, minColor, font=popFont)
textsize = draw.textsize(tempMax, font=popFont)
draw.text(((width // 2) + 4, y + 2), tempMax, red, font=popFont)
return cv
def drawWeather(wi, cv, dt):
draw = ImageDraw.Draw(cv)
width, height = cv.size
for day in range(2):
wi2 = weather2(day, wi)
cv.paste(wi2, (day * wi2.width, 0))
for day in range(6):
wi6 = weather6(day + 1, wi)
cv.paste(wi6, (day * wi6.width + 5, 240))
dt_str = dt.strftime('%H:%M')
dtFont = getFont(fonts.light, fontsize=12)
textsize = draw.textsize(dt_str, font=dtFont)
draw.text((4, 2), dt_str, fontColor, font=dtFont)
nihongoFont = getFont(fonts.nihongo, fontsize=14)
draw.text((4 + textsize[0], 2), "発表", fontColor, font=nihongoFont)
dt_str = datetime.datetime.now().strftime('%H:%M')
textsize = draw.textsize(dt_str, font=dtFont)
draw.text((width - textsize[0] - 2, 2), dt_str, fontColor, font=dtFont)
publishingOffice = wi.weatherInfo[0]["publishingOffice"]
textsize = draw.textsize(publishingOffice, font=nihongoFont)
draw.text(((width - textsize[0]) // 2, 2), publishingOffice, fontColor, font=nihongoFont)
def update(office, area, reversed):
global lastUpdate, previous
global fontColor, background, borderColor, minColor
if reversed:
fontColor, background = white, black
minColor = blue
else:
fontColor, background = black, white
minColor = blue
wi = weatherInfomation(office, area)
reportDatetime = datetime.datetime.fromisoformat(wi.weatherInfo[0]["reportDatetime"])
cv = Image.new("RGB", canvasSize, background)
drawWeather(wi, cv, reportDatetime)
return cv
例:東京都千代田区
office = "130000"
area = "1310100"
cv = update(office, area, reversed=False)
cv.save("tokyo.png", "PNG")
実際に動作させるためには、フォントファイルやPNGファイルの準備が必要です。
$ ls ./fonts
HiraKakuProN-W1-AlphaNum-02.otf Roboto-Thin.ttf
Roboto-Black.ttf weathericons-regular-webfont.ttf
Roboto-Light.ttf x12y16pxMaruMonica.ttf
$ ls ./images
100.svg.png 202.svg.png 308.svg.png 411.svg.png 515.svg.png
101.svg.png 204.svg.png 311.svg.png 413.svg.png 601.svg.png
102.svg.png 210.svg.png 313.svg.png 414.svg.png 610.svg.png
104.svg.png 212.svg.png 314.svg.png 500.svg.png 701.svg.png
110.svg.png 215.svg.png 400.svg.png 501.svg.png 711.svg.png
112.svg.png 300.svg.png 401.svg.png 502.svg.png 801.svg.png
115.svg.png 301.svg.png 402.svg.png 504.svg.png 811.svg.png
200.svg.png 302.svg.png 403.svg.png 510.svg.png
201.svg.png 303.svg.png 406.svg.png 512.svg.png