Make viaggiatreno work by fixing some critical bugs (#160093)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Mattia Monga
2026-01-29 12:41:47 +01:00
committed by GitHub
parent dfe1990484
commit 95014d7e6d
3 changed files with 69 additions and 98 deletions

View File

@@ -4,5 +4,6 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/viaggiatreno",
"iot_class": "cloud_polling",
"quality_scale": "legacy"
"quality_scale": "legacy",
"requirements": ["viaggiatreno_ha==0.2.4"]
}

View File

@@ -2,12 +2,17 @@
from __future__ import annotations
import asyncio
from http import HTTPStatus
from datetime import timedelta
import logging
import time
from typing import Any
import aiohttp
from viaggiatreno_ha.trainline import (
TrainLine,
TrainLineStatus,
TrainState,
Viaggiatreno,
)
import voluptuous as vol
from homeassistant.components.sensor import (
@@ -19,19 +24,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
_LOGGER = logging.getLogger(__name__)
VIAGGIATRENO_ENDPOINT = (
"http://www.viaggiatreno.it/infomobilita/"
"resteasy/viaggiatreno/andamentoTreno/"
"{station_id}/{train_id}/{timestamp}"
)
REQUEST_TIMEOUT = 5 # seconds
ICON = "mdi:train"
MONITORED_INFO = [
MONITORED_INFO = [ # Backward compatibility with older versions
"categoria",
"compOrarioArrivoZeroEffettivo",
"compOrarioPartenzaZeroEffettivo",
@@ -47,7 +45,6 @@ DEFAULT_NAME = "Train {}"
CONF_NAME = "train_name"
CONF_STATION_ID = "station_id"
CONF_STATION_NAME = "station_name"
CONF_TRAIN_ID = "train_id"
ARRIVED_STRING = "Arrived"
@@ -55,6 +52,8 @@ CANCELLED_STRING = "Cancelled"
NOT_DEPARTED_STRING = "Not departed yet"
NO_INFORMATION_STRING = "No information for this train now"
SCAN_INTERVAL = timedelta(minutes=2)
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_TRAIN_ID): cv.string,
@@ -71,126 +70,94 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the ViaggiaTreno platform."""
train_id = config.get(CONF_TRAIN_ID)
station_id = config.get(CONF_STATION_ID)
train_id = str(config.get(CONF_TRAIN_ID))
station_id = str(config.get(CONF_STATION_ID))
if not (name := config.get(CONF_NAME)):
name = DEFAULT_NAME.format(train_id)
async_add_entities([ViaggiaTrenoSensor(train_id, station_id, name)])
async def async_http_request(hass, uri):
"""Perform actual request."""
try:
session = async_get_clientsession(hass)
async with asyncio.timeout(REQUEST_TIMEOUT):
req = await session.get(uri)
if req.status != HTTPStatus.OK:
return {"error": req.status}
json_response = await req.json()
except (TimeoutError, aiohttp.ClientError) as exc:
_LOGGER.error("Cannot connect to ViaggiaTreno API endpoint: %s", exc)
return None
except ValueError:
_LOGGER.error("Received non-JSON data from ViaggiaTreno API endpoint")
return None
return json_response
tl = TrainLine(train_id=train_id, starting_station=station_id)
async_add_entities([ViaggiaTrenoSensor(tl, name)], True)
class ViaggiaTrenoSensor(SensorEntity):
"""Implementation of a ViaggiaTreno sensor."""
_attr_attribution = "Powered by ViaggiaTreno Data"
_attr_should_poll = True
def __init__(self, train_id, station_id, name):
def __init__(self, train_line: TrainLine, name: str) -> None:
"""Initialize the sensor."""
self._state = None
self._attributes = {}
self._unit = ""
self._state: StateType = NO_INFORMATION_STRING
self._attributes: dict[str, Any] = {}
self._icon = ICON
self._station_id = station_id
self._name = name
self.uri = VIAGGIATRENO_ENDPOINT.format(
station_id=station_id, train_id=train_id, timestamp=int(time.time()) * 1000
)
self._line = train_line
self._viaggiatreno: Viaggiatreno | None = None
self._tstatus: TrainLineStatus | None = None
@property
def name(self):
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def native_value(self):
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self._state
@property
def icon(self):
def icon(self) -> str:
"""Icon to use in the frontend, if any."""
return self._icon
@property
def native_unit_of_measurement(self):
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
return self._unit
if isinstance(self.native_value, (int, float)):
return UnitOfTime.MINUTES
return None
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return extra attributes."""
return self._attributes
@staticmethod
def has_departed(data):
"""Check if the train has actually departed."""
try:
first_station = data["fermate"][0]
if data["oraUltimoRilevamento"] or first_station["effettiva"]:
return True
except ValueError:
_LOGGER.error("Cannot fetch first station: %s", data)
return False
@staticmethod
def has_arrived(data):
"""Check if the train has already arrived."""
last_station = data["fermate"][-1]
if not last_station["effettiva"]:
return False
return True
@staticmethod
def is_cancelled(data):
"""Check if the train is cancelled."""
if data["tipoTreno"] == "ST" and data["provvedimento"] == 1:
return True
return False
async def async_update(self) -> None:
"""Update state."""
uri = self.uri
res = await async_http_request(self.hass, uri)
if res.get("error", ""):
if res["error"] == 204:
self._state = NO_INFORMATION_STRING
self._unit = ""
else:
self._state = f"Error: {res['error']}"
self._unit = ""
else:
for i in MONITORED_INFO:
self._attributes[i] = res[i]
if self.is_cancelled(res):
if self._viaggiatreno is None:
session = async_get_clientsession(self.hass)
self._viaggiatreno = Viaggiatreno(session)
try:
await self._viaggiatreno.query_if_useful(self._line)
self._tstatus = self._viaggiatreno.get_line_status(self._line)
if self._tstatus is None:
_LOGGER.error(
"Received status for line %s: None. Check the train and station IDs",
self._line,
)
return
except (TimeoutError, aiohttp.ClientError) as exc:
_LOGGER.error("Cannot connect to ViaggiaTreno API endpoint: %s", exc)
return
except ValueError:
_LOGGER.error("Received non-JSON data from ViaggiaTreno API endpoint")
return
if self._tstatus is not None:
if self._tstatus.state == TrainState.CANCELLED:
self._state = CANCELLED_STRING
self._icon = "mdi:cancel"
self._unit = ""
elif not self.has_departed(res):
elif self._tstatus.state == TrainState.NOT_YET_DEPARTED:
self._state = NOT_DEPARTED_STRING
self._unit = ""
elif self.has_arrived(res):
elif self._tstatus.state == TrainState.ARRIVED:
self._state = ARRIVED_STRING
self._unit = ""
else:
self._state = res.get("ritardo")
self._unit = UnitOfTime.MINUTES
elif self._tstatus.state in {
TrainState.RUNNING,
TrainState.PARTIALLY_CANCELLED,
}:
delay_minutes = self._tstatus.timetable.delay
self._state = delay_minutes
self._icon = ICON
else:
self._state = NO_INFORMATION_STRING
# Update attributes
for info in MONITORED_INFO:
self._attributes[info] = self._viaggiatreno.json[self._line][info]

3
requirements_all.txt generated
View File

@@ -3143,6 +3143,9 @@ velbus-aio==2026.1.4
# homeassistant.components.venstar
venstarcolortouch==0.21
# homeassistant.components.viaggiatreno
viaggiatreno_ha==0.2.4
# homeassistant.components.victron_ble
victron-ble-ha-parser==0.4.9