Compare commits

...

4 commits

Author SHA1 Message Date
eaa32c6987 Add sample config file 2024-01-14 12:34:15 -08:00
03f05fe44c Add reverse geocoding 2024-01-14 12:27:02 -08:00
d4a4d5a18c Update requirements 2024-01-14 12:25:56 -08:00
3ea6abd7f9 Exclude config.yml 2024-01-14 12:25:35 -08:00
4 changed files with 125 additions and 23 deletions

2
.gitignore vendored
View file

@ -166,3 +166,5 @@ cython_debug/
*.secret
*.db
.DS_Store
config.yml

9
config-sample.yml Normal file
View file

@ -0,0 +1,9 @@
stadiamaps:
api_key:
nominatim:
api_base_url:
mastodon:
client_id:
client_secret:
access_token:
api_base_url:

View file

@ -1,4 +1,3 @@
beautifulsoup4==4.12.2
blurhash==1.1.4
certifi==2023.11.17
charset-normalizer==3.3.2
@ -9,9 +8,9 @@ Mastodon.py==1.8.1
pillow==10.2.0
python-dateutil==2.8.2
python-magic==0.4.27
PyYAML==6.0.1
requests==2.28.2
six==1.16.0
soupsieve==2.5
SQLAlchemy==2.0.25
staticmap==0.5.7
typing_extensions==4.9.0

134
scl.py
View file

@ -1,8 +1,10 @@
import io
import math
from datetime import datetime
from typing import Optional
import requests
import yaml
from mastodon import Mastodon
from PIL import Image, ImageDraw, ImageFont
from sqlalchemy import create_engine, select
@ -20,11 +22,15 @@ except requests.JSONDecodeError:
print("JSON could not be loaded from SCL API")
raise
mastodon = Mastodon(access_token="scl_bot_mastodon.secret")
with open("stadiamaps_api_key.secret", "r+") as stadiamaps_api_key_file:
# Reading from a file
stadiamaps_api_key = stadiamaps_api_key_file.read()
config = yaml.safe_load(open("config.yml"))
stadiamaps_api_key = config["stadiamaps"]["api_key"]
nominatim_url = config["nominatim"]["api_base_url"]
mastodon = Mastodon(
client_id=config["mastodon"]["client_id"],
client_secret=config["mastodon"]["client_secret"],
access_token=config["mastodon"]["access_token"],
api_base_url=config["mastodon"]["api_base_url"],
)
class AttribStaticMap(StaticMap, object):
@ -181,22 +187,7 @@ with Session(engine) as session:
# If the outage becomes medium/large, it'll then be posted as a new outage on the next run
print("Outage is small, will not post")
continue
print("Existing record not found")
post_text = """Seattle City Light is reporting a {} outage in {}.
Start Date: {}
Est. Restoration: {}
Cause: {}
{}""".format(
outage_size.lower(),
event["city"],
start_time.strftime(post_datetime_format),
estimated_restoration_time.strftime(post_datetime_format),
event["cause"],
hashtag_string,
)
try:
map = AttribStaticMap(
@ -212,10 +203,92 @@ Cause: {}
)
map.add_polygon(polygon)
map_image = map.render()
try:
def num2deg(xtile, ytile, zoom):
n = 1 << zoom
lon_deg = xtile / n * 360.0 - 180.0
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
lat_deg = math.degrees(lat_rad)
return lat_deg, lon_deg
center_lat_lon = num2deg(map.x_center, map.y_center, map.zoom)
# Zoom level 17 ensures that we won't get any building/POI names, just street names
geocode_url = "{nominatim_url}/reverse?lat={lat}&lon={lon}&format=geocodejson&zoom=17".format(
nominatim_url=nominatim_url,
lat=center_lat_lon[0],
lon=center_lat_lon[1],
)
geocode_headers = {"User-Agent": "seattlecitylight-mastodon-bot"}
geocode_response = requests.get(
geocode_url, headers=geocode_headers
)
try:
geocode = geocode_response.json()
except requests.JSONDecodeError:
print("JSON could not be loaded from nominatim API")
raise
if (
geocode["features"][0]["properties"]["geocoding"]["city"]
!= "Seattle"
):
city_not_seattle_text = " of {}".format(
geocode["features"][0]["properties"]["geocoding"]["city"]
)
else:
city_not_seattle_text = ""
street = geocode["features"][0]["properties"]["geocoding"]["name"]
if "locality" in geocode["features"][0]["properties"]["geocoding"]:
locality = geocode["features"][0]["properties"]["geocoding"]
if locality == "Uptown":
locality = "Lower Queen Anne"
alt_text = "A map showing the location of the outage, centered around {} in the {} area{}.".format(
street,
locality,
city_not_seattle_text,
)
area_text = "the {} area{}".format(
locality, city_not_seattle_text
)
elif (
"district" in geocode["features"][0]["properties"]["geocoding"]
):
alt_text = "A map showing the location of the outage, centered around {} in the {} area{}.".format(
street,
geocode["features"][0]["properties"]["geocoding"][
"district"
],
city_not_seattle_text,
)
area_text = "the {} area{}".format(
geocode["features"][0]["properties"]["geocoding"][
"district"
],
city_not_seattle_text,
)
else:
alt_text = "A map showing the location of the outage, centered around {} in {}.".format(
street,
geocode["features"][0]["properties"]["geocoding"]["city"],
)
area_text = geocode["features"][0]["properties"]["geocoding"][
"city"
]
except Exception:
alt_text = "A map showing the location of the outage."
with io.BytesIO() as map_image_file:
map_image.save(map_image_file, format="PNG", optimize=True)
map_media_post = mastodon.media_post(
map_image_file.getvalue(), mime_type="image/png"
map_image_file.getvalue(),
mime_type="image/png",
description=alt_text,
)
except Exception as e:
@ -225,6 +298,25 @@ Cause: {}
)
map_media_post = None
# Fallback location from the SCL API in case one couldn't be reverse geocoded
if not area_text:
area_text = event["city"]
post_text = """Seattle City Light is reporting a {} outage in {}.
Start Date: {}
Est. Restoration: {}
Cause: {}
{}""".format(
outage_size.lower(),
area_text,
start_time.strftime(post_datetime_format),
estimated_restoration_time.strftime(post_datetime_format),
event["cause"],
hashtag_string,
)
mastodon_post_result = mastodon.status_post(
status=post_text,
media_ids=map_media_post,