├── hacs.json ├── custom_components └── metar │ ├── manifest.json │ ├── __init__.py │ ├── const.py │ ├── translations │ ├── en.json │ └── it.json │ ├── config_flow.py │ ├── metar_data.py │ └── sensor.py └── README.md /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homeassistant metar temperature sensor", 3 | "render_readme": "true", 4 | "country": "NO" 5 | } 6 | -------------------------------------------------------------------------------- /custom_components/metar/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "metar", 3 | "name": "METAR Sensor", 4 | "version": "1.0.0", 5 | "documentation": "https://github.com/lfasci/metar", 6 | "requirements": ["python-metar"], 7 | "codeowners": ["@lfasci"], 8 | "config_flow": true 9 | } -------------------------------------------------------------------------------- /custom_components/metar/__init__.py: -------------------------------------------------------------------------------- 1 | from homeassistant.config_entries import ConfigEntry 2 | from homeassistant.core import HomeAssistant 3 | 4 | DOMAIN = "metar" 5 | 6 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 7 | await hass.config_entries.async_forward_entry_setups(entry, ["sensor"]) 8 | return True -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homeassistant-metar 2 | **METAR** is an abbreviation for **Meteorological Aerodrome Repor. It is a routine observation of the current weather conditions at an airport or aerodrome, produced regularly. This is a sensor for METAR temperatures. https://en.wikipedia.org/wiki/METAR 3 | 4 | ## Configuration 5 | 6 | The configuration is now managed using the UI from: Settings-> Devices & Services -> METAR Sensor 7 | 8 | Old configurations in configuration.yml are ignored anc can be removed 9 | 10 | -------------------------------------------------------------------------------- /custom_components/metar/const.py: -------------------------------------------------------------------------------- 1 | DOMAIN = "metar" 2 | 3 | CONF_AIRPORT_NAME = "airport_name" 4 | CONF_AIRPORT_CODE = "airport_code" 5 | CONF_MONITORED_CONDITIONS = "monitored_conditions" 6 | 7 | DEFAULT_MONITORED = ["time", "temperature", "wind", "pressure", "visibility", "precipitation", "sky"] 8 | 9 | SENSOR_TYPES = { 10 | "time": ["Updated", None], 11 | "weather": ["Condition", None], 12 | "temperature": ["Temperature", "C"], 13 | "wind": ["Wind speed", None], 14 | "pressure": ["Pressure", None], 15 | "visibility": ["Visibility", "m"], 16 | "precipitation": ["Precipitation", "in"], 17 | "sky": ["Sky", None] 18 | } -------------------------------------------------------------------------------- /custom_components/metar/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Configure METAR Airport", 6 | "description": "Set the airport details and choose which conditions to monitor" 7 | } 8 | }, 9 | "error": { 10 | "cannot_connect": "Failed to connect to METAR data source", 11 | "invalid_airport": "Invalid airport code or no data available" 12 | }, 13 | "abort": { 14 | "already_configured": "This airport is already configured" 15 | } 16 | }, 17 | "options": { 18 | "step": { 19 | "init": { 20 | "title": "Update monitored conditions" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/metar/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Configura aeroporto METAR", 6 | "description": "Imposta i dettagli dell'aeroporto e scegli le condizioni da monitorare" 7 | } 8 | }, 9 | "error": { 10 | "cannot_connect": "Impossibile connettersi alla fonte dati METAR", 11 | "invalid_airport": "Codice aeroporto non valido o dati non disponibili" 12 | }, 13 | "abort": { 14 | "already_configured": "Questo aeroporto è già configurato" 15 | } 16 | }, 17 | "options": { 18 | "step": { 19 | "init": { 20 | "title": "Aggiorna le condizioni monitorate" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /custom_components/metar/config_flow.py: -------------------------------------------------------------------------------- 1 | import voluptuous as vol 2 | from homeassistant import config_entries 3 | import homeassistant.helpers.config_validation as cv 4 | from .const import DOMAIN, CONF_AIRPORT_NAME, CONF_AIRPORT_CODE, CONF_MONITORED_CONDITIONS, SENSOR_TYPES 5 | 6 | class MetarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 7 | async def async_step_user(self, user_input=None): 8 | if user_input is not None: 9 | return self.async_create_entry(title=user_input[CONF_AIRPORT_NAME], data=user_input) 10 | 11 | return self.async_show_form( 12 | step_id="user", 13 | data_schema=vol.Schema({ 14 | vol.Required(CONF_AIRPORT_NAME, default="Pisa"): str, 15 | vol.Required(CONF_AIRPORT_CODE, default="LIRP"): str, 16 | vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES.keys())): 17 | cv.multi_select(list(SENSOR_TYPES.keys())) 18 | }) 19 | ) -------------------------------------------------------------------------------- /custom_components/metar/metar_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | from aiohttp import ClientSession 4 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 5 | from homeassistant.util import Throttle 6 | from metar import Metar 7 | 8 | BASE_URL = "https://tgftp.nws.noaa.gov/data/observations/metar/stations/" 9 | SCAN_INTERVAL = timedelta(seconds=3600) 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | class MetarData: 13 | def __init__(self, hass, airport): 14 | self._airport_code = airport["code"] 15 | self._session: ClientSession = async_get_clientsession(hass) 16 | self.sensor_data = None 17 | 18 | @Throttle(SCAN_INTERVAL) 19 | async def async_update(self): 20 | url = f"{BASE_URL}{self._airport_code}.TXT" 21 | try: 22 | async with self._session.get(url) as response: 23 | response.raise_for_status() 24 | text = await response.text() 25 | 26 | for line in text.splitlines(): 27 | if line.startswith(self._airport_code): 28 | self.sensor_data = Metar.Metar(line) 29 | _LOGGER.info("METAR Data: %s", self.sensor_data.string()) 30 | return 31 | 32 | _LOGGER.error("No METAR data found for %s", self._airport_code) 33 | except Exception as exc: 34 | _LOGGER.error("Error retrieving METAR data for %s: %s", self._airport_code, exc) -------------------------------------------------------------------------------- /custom_components/metar/sensor.py: -------------------------------------------------------------------------------- 1 | from homeassistant.helpers.entity import Entity 2 | from .const import SENSOR_TYPES, CONF_AIRPORT_NAME, CONF_AIRPORT_CODE, CONF_MONITORED_CONDITIONS 3 | from .metar_data import MetarData 4 | 5 | async def async_setup_entry(hass, config_entry, async_add_entities): 6 | config = config_entry.data 7 | airport = { 8 | "location": config[CONF_AIRPORT_NAME], 9 | "code": config[CONF_AIRPORT_CODE] 10 | } 11 | 12 | data = MetarData(hass, airport) 13 | sensors = [ 14 | MetarSensor(airport, data, sensor_type, SENSOR_TYPES[sensor_type][1]) 15 | for sensor_type in config.get(CONF_MONITORED_CONDITIONS, []) 16 | ] 17 | 18 | async_add_entities(sensors, True) 19 | 20 | class MetarSensor(Entity): 21 | def __init__(self, airport, weather_data, sensor_type, unit_of_measurement): 22 | self._state = None 23 | self._name = SENSOR_TYPES[sensor_type][0] 24 | self._unit_of_measurement = unit_of_measurement 25 | self._airport_name = airport["location"] 26 | self.type = sensor_type 27 | self.weather_data = weather_data 28 | 29 | @property 30 | def name(self): 31 | return f"{self._name} {self._airport_name}" 32 | 33 | @property 34 | def state(self): 35 | return self._state 36 | 37 | @property 38 | def unit_of_measurement(self): 39 | return self._unit_of_measurement 40 | 41 | async def async_update(self): 42 | await self.weather_data.async_update() 43 | metar = self.weather_data.sensor_data 44 | 45 | if not metar: 46 | self._state = None 47 | return 48 | 49 | try: 50 | if self.type == "time": 51 | self._state = metar.time.ctime() 52 | elif self.type == "temperature": 53 | self._state = metar.temp.string().split(" ")[0] 54 | elif self.type == "weather": 55 | self._state = metar.present_weather() 56 | elif self.type == "wind": 57 | self._state = metar.wind() 58 | elif self.type == "pressure": 59 | self._state = metar.press.string("mb") 60 | elif self.type == "visibility": 61 | self._state = metar.visibility() 62 | elif self.type == "precipitation": 63 | self._state = metar.precip_1hr 64 | elif self.type == "sky": 65 | self._state = metar.sky_conditions("\n ") 66 | except Exception: 67 | self._state = None 68 | --------------------------------------------------------------------------------