diff --git a/.coveragerc b/.coveragerc index 773bc9fb0f6..a5ff56b8d04 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,6 +374,7 @@ omit = homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/cover.py homeassistant/components/hunterdouglas_powerview/entity.py + homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index 853ed9460c8..e003b25ea85 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -1,6 +1,7 @@ """The HVV integration.""" import asyncio +from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME @@ -10,7 +11,7 @@ from homeassistant.helpers import aiohttp_client from .const import DOMAIN from .hub import GTIHub -PLATFORMS = [DOMAIN_SENSOR] +PLATFORMS = [DOMAIN_SENSOR, DOMAIN_BINARY_SENSOR] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py new file mode 100644 index 00000000000..7d19fcc8fdf --- /dev/null +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -0,0 +1,201 @@ +"""Binary sensor platform for hvv_departures.""" +from datetime import timedelta +import logging + +from aiohttp import ClientConnectorError +import async_timeout +from pygti.exceptions import InvalidAuth + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ATTRIBUTION, CONF_STATION, DOMAIN, MANUFACTURER + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the binary_sensor platform.""" + hub = hass.data[DOMAIN][entry.entry_id] + station_name = entry.data[CONF_STATION]["name"] + station = entry.data[CONF_STATION] + + def get_elevator_entities_from_station_information( + station_name, station_information + ): + """Convert station information into a list of elevators.""" + elevators = {} + + if station_information is None: + return {} + + for partial_station in station_information.get("partialStations", []): + for elevator in partial_station.get("elevators", []): + + state = elevator.get("state") != "READY" + available = elevator.get("state") != "UNKNOWN" + label = elevator.get("label") + description = elevator.get("description") + + if label is not None: + name = f"Elevator {label} at {station_name}" + else: + name = f"Unknown elevator at {station_name}" + + if description is not None: + name += f" ({description})" + + lines = elevator.get("lines") + + idx = f"{station_name}-{label}-{lines}" + + elevators[idx] = { + "state": state, + "name": name, + "available": available, + "attributes": { + "cabin_width": elevator.get("cabinWidth"), + "cabin_length": elevator.get("cabinLength"), + "door_width": elevator.get("doorWidth"), + "elevator_type": elevator.get("elevatorType"), + "button_type": elevator.get("buttonType"), + "cause": elevator.get("cause"), + "lines": lines, + ATTR_ATTRIBUTION: ATTRIBUTION, + }, + } + return elevators + + async def async_update_data(): + """Fetch data from API endpoint. + + This is the place to pre-process the data to lookup tables + so entities can quickly look up their data. + """ + + payload = {"station": station} + + try: + async with async_timeout.timeout(10): + return get_elevator_entities_from_station_information( + station_name, await hub.gti.stationInformation(payload) + ) + except InvalidAuth as err: + raise UpdateFailed(f"Authentication failed: {err}") from err + except ClientConnectorError as err: + raise UpdateFailed(f"Network not available: {err}") from err + except Exception as err: # pylint: disable=broad-except + raise UpdateFailed(f"Error occurred while fetching data: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="hvv_departures.binary_sensor", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(hours=1), + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + async_add_entities( + HvvDepartureBinarySensor(coordinator, idx, entry) + for (idx, ent) in coordinator.data.items() + ) + + +class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): + """HVVDepartureBinarySensor class.""" + + def __init__(self, coordinator, idx, config_entry): + """Initialize.""" + super().__init__(coordinator) + self.coordinator = coordinator + self.idx = idx + self.config_entry = config_entry + + @property + def is_on(self): + """Return entity state.""" + return self.coordinator.data[self.idx]["state"] + + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def available(self): + """Return if entity is available.""" + return ( + self.coordinator.last_update_success + and self.coordinator.data[self.idx]["available"] + ) + + @property + def device_info(self): + """Return the device info for this sensor.""" + return { + "identifiers": { + ( + DOMAIN, + self.config_entry.entry_id, + self.config_entry.data[CONF_STATION]["id"], + self.config_entry.data[CONF_STATION]["type"], + ) + }, + "name": f"Departures at {self.config_entry.data[CONF_STATION]['name']}", + "manufacturer": MANUFACTURER, + } + + @property + def name(self): + """Return the name of the sensor.""" + return self.coordinator.data[self.idx]["name"] + + @property + def unique_id(self): + """Return a unique ID to use for this sensor.""" + return self.idx + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_PROBLEM + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if not ( + self.coordinator.last_update_success + and self.coordinator.data[self.idx]["available"] + ): + return None + return { + k: v + for k, v in self.coordinator.data[self.idx]["attributes"].items() + if v is not None + } + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 1646ee73dbd..bd3e955d2d8 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -267,7 +267,7 @@ async def test_options_flow(hass): ) config_entry.add_to_hass(hass) - with patch( + with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True, ), patch( @@ -318,7 +318,7 @@ async def test_options_flow_invalid_auth(hass): ) config_entry.add_to_hass(hass) - with patch( + with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True ), patch( "homeassistant.components.hvv_departures.hub.GTI.departureList", @@ -359,7 +359,7 @@ async def test_options_flow_cannot_connect(hass): ) config_entry.add_to_hass(hass) - with patch( + with patch("homeassistant.components.hvv_departures.PLATFORMS", new=[]), patch( "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True ), patch( "homeassistant.components.hvv_departures.hub.GTI.departureList",