home-assistant/core

The Trenitalia ViaggiaTreno integration does not work

iaxexo opened this issue · 3 comments

iaxexo commented

The problem

The Trenitalia ViaggiaTreno integration expects the API to return a numeric value, but it is not always the case. It returns an error message.

Probably the default type of the sensor changed to numeric some times ago and it broke the integration without anybody noticing it.

What version of Home Assistant Core has the issue?

core-2024.10.3

What was the last working version of Home Assistant Core?

not available

What type of installation are you running?

Home Assistant OS

Integration causing the issue

The Trenitalia ViaggiaTreno integration

Link to integration documentation on our website

https://www.home-assistant.io/integrations/viaggiatreno/

Diagnostics information

No response

Example YAML snippet

paste this in configursation.yaml to replicate the issue

sensor:
  - platform: viaggiatreno
    train_id: 26397
    station_id: S00208

Anything in the logs that might be useful for us?

Logger: homeassistant
Source: components/sensor/__init__.py:664
First occurred: 10:34:59 (3 occurrences)
Last logged: 10:35:59

Error doing job: Task exception was never retrieved (None)
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/sensor/__init__.py", line 662, in state
    numerical_value = float(value)  # type:ignore[arg-type]
                      ^^^^^^^^^^^^
ValueError: could not convert string to float: 'Not departed yet'

The above exception was the direct cause of the following exception:

> Traceback (most recent call last):
>   File "/usr/src/homeassistant/homeassistant/helpers/entity_platform.py", line 1047, in _async_update_entity_states
    await asyncio.gather(*tasks)
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 960, in async_update_ha_state
    self._async_write_ha_state()
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1130, in _async_write_ha_state
    self.__async_calculate_state()
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1067, in __async_calculate_state
    state = self._stringify_state(available)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1011, in _stringify_state
    if (state := self.state) is None:
                 ^^^^^^^^^^
   File "/usr/src/homeassistant/homeassistant/components/sensor/__init__.py", line 664, in state
    raise ValueError(
ValueError: Sensor sensor.train_26376 has device class 'None', state class 'None' unit '' and suggested precision 'None' thus indicating it has a numeric value; however, it has the non-numeric value: 'Not departed yet' (<class 'str'>)

Additional information

No response

iaxexo commented

Since I am the only user of such integration and since the issue seems fairly simple, I may try to fix that on my own

iaxexo commented

Commenting the 5 lines highlighted with # below solved the issue to me:

"""Support for the Italian train system using ViaggiaTreno API."""

from __future__ import annotations

import asyncio
from http import HTTPStatus
import logging
import time

import aiohttp
import voluptuous as vol

from homeassistant.components.sensor import (
    PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
    SensorEntity,
)
from homeassistant.const import UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

_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 = [
    "categoria",
    "compOrarioArrivoZeroEffettivo",
    "compOrarioPartenzaZeroEffettivo",
    "destinazione",
    "numeroTreno",
    "orarioArrivo",
    "orarioPartenza",
    "origine",
    "subTitle",
]

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"
CANCELLED_STRING = "Cancelled"
NOT_DEPARTED_STRING = "Not departed yet"
NO_INFORMATION_STRING = "No information for this train now"

PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_TRAIN_ID): cv.string,
        vol.Required(CONF_STATION_ID): cv.string,
        vol.Optional(CONF_NAME): cv.string,
    }
)


async def async_setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    async_add_entities: AddEntitiesCallback,
    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)
    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


class ViaggiaTrenoSensor(SensorEntity):
    """Implementation of a ViaggiaTreno sensor."""

    _attr_attribution = "Powered by ViaggiaTreno Data"

    def __init__(self, train_id, station_id, name):
        """Initialize the sensor."""
        self._state = None
        self._attributes = {}
#      self._unit = ""
        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
        )

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def native_value(self):
        """Return the state of the sensor."""
        return self._state

    @property
    def icon(self):
        """Icon to use in the frontend, if any."""
        return self._icon

#    @property
#    def native_unit_of_measurement(self):
#        """Return the unit of measurement."""
#        return self._unit

    @property
    def extra_state_attributes(self):
        """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):
                self._state = CANCELLED_STRING
                self._icon = "mdi:cancel"
                self._unit = ""
            elif not self.has_departed(res):
                self._state = NOT_DEPARTED_STRING
                self._unit = ""
            elif self.has_arrived(res):
                self._state = ARRIVED_STRING
                self._unit = ""
            else:
                self._state = res.get("ritardo")
                self._unit = UnitOfTime.MINUTES
                self._icon = ICON