Source code for pyiem.nws.products.metar_util
"""METAR formatting utilities."""
from datetime import timezone
from typing import Optional
import numpy as np
from metpy.units import units
from pyiem.reference import VARIABLE_WIND_DIRECTION
[docs]
def metar_from_dict(ob: dict) -> str:
"""Format a METAR from a dictionary like pyiem.Observation."""
tokens = [
f"METAR {ob['station']}",
f"{ob['valid'].astimezone(timezone.utc):%d%H%M}Z AUTO",
metar_format_wind(ob["drct"], ob["sknt"], ob["gust"]),
metar_format_visibility(ob["vsby"]),
metar_format_weather(ob["wxcodes"]),
metar_format_sky(ob),
metar_format_temperature(ob["tmpf"], ob["dwpf"]),
metar_format_altimeter(ob["alti"]),
"RMK AO2",
metar_format_mslp(ob["mslp"]),
metar_format_pgroup(ob["phour"], 1),
metar_format_pgroup(ob["p03i"], 3),
metar_format_pgroup(ob["p06i"], 6),
metar_format_pgroup(ob["p24i"], 24),
metar_format_temperature(ob["tmpf"], ob["dwpf"], tgroup=True),
]
# Remove None values
tokens = [t for t in tokens if t is not None]
return " ".join(tokens)
[docs]
def metar_format_altimeter(alti: Optional[float]) -> Optional[str]:
"""Format the altimeter."""
if alti is None:
return None
return f"A{alti * 100.0:.0f}"
[docs]
def metar_format_mslp(mslp: Optional[float]) -> Optional[str]:
"""Format the SLP value."""
if mslp is None:
return None
if mslp >= 1000:
return f"SLP{(mslp * 10.0) - 10_000:03.0f}"
return f"SLP{(mslp * 10.0) - 9_000:03.0f}"
[docs]
def metar_format_pgroup(
phour: Optional[float], hours: Optional[int] = 1
) -> Optional[str]:
"""Format the precipitation group."""
if phour is None or phour <= 0:
return None
prefix = "P" if hours == 1 else "6"
if hours == 24:
prefix = "7"
if phour < 0.005:
return f"{prefix}0000"
return f"{prefix}{phour * 100.0:04.0f}"
[docs]
def metar_format_sky(ob: dict) -> Optional[str]:
"""Format the sky conditions.
Args:
ob (dict): Containing skyc{1,2,3,4} and skyl{1,2,3,4} keys
"""
res = []
for i in range(1, 5):
skyc = ob.get(f"skyc{i}")
skyl = ob.get(f"skyl{i}")
if skyc is None:
continue
if skyc == "CLR":
res.append("CLR")
elif skyl is not None:
skyl = int(np.round(skyl, 0))
res.append(f"{skyc}{(skyl / 100.0):03.0f}")
if not res:
return None
return " ".join(res)
[docs]
def metar_format_temperature(
tmpf: Optional[float], dwpf: Optional[float], tgroup: bool = False
) -> Optional[str]:
"""Format the temperature."""
# Understanding is that temperature is required
if tmpf is None:
return None
tmpc = (units.degF * tmpf).to(units.degC).m
df = "M" if tmpc < 0 else ""
tf = "1" if tmpc < 0 else "0"
metarmsg = f"{df}{abs(tmpc):02.0f}/"
tmsg = f"T{tf}{abs(tmpc) * 10.0:03.0f}"
if dwpf is not None:
dwpc = (units.degF * dwpf).to(units.degC).m
df = "M" if dwpc < 0 else ""
tf = "1" if dwpc < 0 else "0"
metarmsg += f"{df}{abs(dwpc):02.0f}"
tmsg += f"{tf}{abs(dwpc) * 10.0:03.0f}"
return tmsg if tgroup else metarmsg
[docs]
def metar_format_visibility(vsby: Optional[float]) -> Optional[str]:
"""Format the visibility."""
if vsby is None:
return None
res = ""
if vsby == 0:
res = "0"
elif vsby < 0.07:
res = "1/16" # Check this for M1/16
elif vsby < 0.13:
res = "1/8"
elif vsby < 0.26:
res = "1/4"
elif vsby < 0.38:
res = "3/8"
elif vsby < 0.51:
res = "1/2"
elif vsby < 1.1:
res = "1"
elif vsby < 1.6:
res = "1 1/2"
elif vsby < 2.1:
res = "2"
elif vsby < 2.6:
res = "2 1/2"
else:
res = f"{vsby:.0f}"
return f"{res}SM"
[docs]
def metar_format_weather(wxcodes: Optional[list]) -> Optional[str]:
"""Format the present weather strings."""
if wxcodes is None or not wxcodes:
return None
return " ".join(wxcodes)
[docs]
def metar_format_wind(
drct: Optional[float], sknt: Optional[float], gust: Optional[float]
) -> str:
"""Format the wind speed and direction."""
res = ""
# Wind Direction
if drct is None:
res += "///"
elif drct == VARIABLE_WIND_DIRECTION:
res += "VRB"
else:
res += f"{drct:03.0f}"
if sknt is None:
res += "//KT"
else:
res += f"{sknt:02.0f}"
if gust is not None:
res += f"G{gust:02.0f}"
res += "KT"
return res