From bef512c4255e117680c5cf7024f79b217ce73b10 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 30 Jun 2022 07:09:52 +0200 Subject: [PATCH] Split attributes into sensors for here_travel_time (#72405) --- .../components/here_travel_time/__init__.py | 4 +- .../components/here_travel_time/const.py | 11 +- .../components/here_travel_time/sensor.py | 178 ++++++++++++------ .../here_travel_time/test_sensor.py | 114 ++++++----- 4 files changed, 187 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 2b9853c3b10..24da0bc2673 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -187,8 +187,8 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): return HERERoutingData( { ATTR_ATTRIBUTION: attribution, - ATTR_DURATION: summary["baseTime"] / 60, # type: ignore[misc] - ATTR_DURATION_IN_TRAFFIC: traffic_time / 60, + ATTR_DURATION: round(summary["baseTime"] / 60), # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: round(traffic_time / 60), ATTR_DISTANCE: distance, ATTR_ROUTE: response.route_short, ATTR_ORIGIN: ",".join(origin), diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index bde17f5c306..b3768b2d69d 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -24,8 +24,6 @@ CONF_DEPARTURE_TIME = "departure_time" DEFAULT_NAME = "HERE Travel Time" -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] - TRAVEL_MODE_BICYCLE = "bicycle" TRAVEL_MODE_CAR = "car" TRAVEL_MODE_PEDESTRIAN = "pedestrian" @@ -41,7 +39,6 @@ TRAVEL_MODES = [ TRAVEL_MODE_TRUCK, ] -TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] TRAFFIC_MODE_ENABLED = "traffic_enabled" @@ -58,6 +55,14 @@ ICON_PEDESTRIAN = "mdi:walk" ICON_PUBLIC = "mdi:bus" ICON_TRUCK = "mdi:truck" +ICONS = { + TRAVEL_MODE_BICYCLE: ICON_BICYCLE, + TRAVEL_MODE_PEDESTRIAN: ICON_PEDESTRIAN, + TRAVEL_MODE_PUBLIC: ICON_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE: ICON_PUBLIC, + TRAVEL_MODE_TRUCK: ICON_TRUCK, +} + UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] ATTR_DURATION = "duration" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 75c9fd2ea3b..c4be60d5569 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,16 +1,24 @@ """Support for HERE travel time sensors.""" from __future__ import annotations +from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_MODE, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, @@ -28,10 +36,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import HereTravelTimeDataUpdateCoordinator from .const import ( + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, ATTR_DURATION, ATTR_DURATION_IN_TRAFFIC, - ATTR_TRAFFIC_MODE, - ATTR_UNIT_SYSTEM, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + ATTR_ROUTE, CONF_ARRIVAL, CONF_DEPARTURE, CONF_DESTINATION_ENTITY_ID, @@ -44,14 +56,10 @@ from .const import ( CONF_TRAFFIC_MODE, DEFAULT_NAME, DOMAIN, - ICON_BICYCLE, ICON_CAR, - ICON_PEDESTRIAN, - ICON_PUBLIC, - ICON_TRUCK, + ICONS, ROUTE_MODE_FASTEST, ROUTE_MODES, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, @@ -59,7 +67,6 @@ from .const import ( TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, TRAVEL_MODES, - TRAVEL_MODES_PUBLIC, UNITS, ) @@ -115,6 +122,69 @@ PLATFORM_SCHEMA = vol.All( ) +def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]: + """Construct SensorEntityDescriptions.""" + return ( + SensorEntityDescription( + name="Duration", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + name="Duration in Traffic", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DURATION_IN_TRAFFIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + name="Distance", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + name="Route", + icon="mdi:directions", + key=ATTR_ROUTE, + ), + ) + + +def create_origin_sensor( + config_entry: ConfigEntry, hass: HomeAssistant +) -> OriginSensor: + """Create a origin sensor.""" + return OriginSensor( + config_entry.entry_id, + config_entry.data[CONF_NAME], + SensorEntityDescription( + name="Origin", + icon="mdi:store-marker", + key=ATTR_ORIGIN_NAME, + ), + hass.data[DOMAIN][config_entry.entry_id], + ) + + +def create_destination_sensor( + config_entry: ConfigEntry, hass: HomeAssistant +) -> DestinationSensor: + """Create a destination sensor.""" + return DestinationSensor( + config_entry.entry_id, + config_entry.data[CONF_NAME], + SensorEntityDescription( + name="Destination", + icon="mdi:store-marker", + key=ATTR_DESTINATION_NAME, + ), + hass.data[DOMAIN][config_entry.entry_id], + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -143,16 +213,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add HERE travel time entities from a config_entry.""" - async_add_entities( - [ + + sensors: list[HERETravelTimeSensor] = [] + for sensor_description in sensor_descriptions(config_entry.data[CONF_MODE]): + sensors.append( HERETravelTimeSensor( config_entry.entry_id, config_entry.data[CONF_NAME], - config_entry.options[CONF_TRAFFIC_MODE], + sensor_description, hass.data[DOMAIN][config_entry.entry_id], ) - ], - ) + ) + sensors.append(create_origin_sensor(config_entry, hass)) + sensors.append(create_destination_sensor(config_entry, hass)) + async_add_entities(sensors) class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): @@ -162,15 +236,14 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self, unique_id_prefix: str, name: str, - traffic_mode: str, + sensor_description: SensorEntityDescription, coordinator: HereTravelTimeDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._traffic_mode = traffic_mode == TRAFFIC_MODE_ENABLED - self._attr_native_unit_of_measurement = TIME_MINUTES - self._attr_name = name - self._attr_unique_id = unique_id_prefix + self.entity_description = sensor_description + self._attr_name = f"{name} {sensor_description.name}" + self._attr_unique_id = f"{unique_id_prefix}_{sensor_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id_prefix)}, entry_type=DeviceEntryType.SERVICE, @@ -188,34 +261,10 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self.async_on_remove(async_at_start(self.hass, _update_at_start)) @property - def native_value(self) -> str | None: + def native_value(self) -> str | float | None: """Return the state of the sensor.""" if self.coordinator.data is not None: - return str( - round( - self.coordinator.data.get( - ATTR_DURATION_IN_TRAFFIC - if self._traffic_mode - else ATTR_DURATION - ) - ) - ) - return None - - @property - def extra_state_attributes( - self, - ) -> dict[str, None | float | str | bool] | None: - """Return the state attributes.""" - if self.coordinator.data is not None: - res = { - ATTR_UNIT_SYSTEM: self.coordinator.config.units, - ATTR_MODE: self.coordinator.config.travel_mode, - ATTR_TRAFFIC_MODE: self._traffic_mode, - **self.coordinator.data, - } - res.pop(ATTR_ATTRIBUTION) - return res + return self.coordinator.data.get(self.entity_description.key) return None @property @@ -225,15 +274,30 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): return self.coordinator.data.get(ATTR_ATTRIBUTION) return None + +class OriginSensor(HERETravelTimeSensor): + """Sensor holding information about the route origin.""" + @property - def icon(self) -> str: - """Icon to use in the frontend depending on travel_mode.""" - if self.coordinator.config.travel_mode == TRAVEL_MODE_BICYCLE: - return ICON_BICYCLE - if self.coordinator.config.travel_mode == TRAVEL_MODE_PEDESTRIAN: - return ICON_PEDESTRIAN - if self.coordinator.config.travel_mode in TRAVEL_MODES_PUBLIC: - return ICON_PUBLIC - if self.coordinator.config.travel_mode == TRAVEL_MODE_TRUCK: - return ICON_TRUCK - return ICON_CAR + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """GPS coordinates.""" + if self.coordinator.data is not None: + return { + ATTR_LATITUDE: self.coordinator.data[ATTR_ORIGIN].split(",")[0], + ATTR_LONGITUDE: self.coordinator.data[ATTR_ORIGIN].split(",")[1], + } + return None + + +class DestinationSensor(HERETravelTimeSensor): + """Sensor holding information about the route destination.""" + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """GPS coordinates.""" + if self.coordinator.data is not None: + return { + ATTR_LATITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[0], + ATTR_LONGITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[1], + } + return None diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index dc9ba128c35..93fbd1bd204 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -7,14 +7,6 @@ import pytest from homeassistant.components.here_travel_time.config_flow import default_options from homeassistant.components.here_travel_time.const import ( - ATTR_DESTINATION, - ATTR_DESTINATION_NAME, - ATTR_DISTANCE, - ATTR_DURATION, - ATTR_DURATION_IN_TRAFFIC, - ATTR_ORIGIN, - ATTR_ORIGIN_NAME, - ATTR_ROUTE, CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, @@ -24,7 +16,6 @@ from homeassistant.components.here_travel_time.const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, CONF_UNIT_SYSTEM, DOMAIN, ICON_BICYCLE, @@ -34,18 +25,18 @@ from homeassistant.components.here_travel_time.const import ( ICON_TRUCK, NO_ROUTE_ERROR_MESSAGE, ROUTE_MODE_FASTEST, - TRAFFIC_MODE_DISABLED, TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, - TRAVEL_MODES_VEHICLE, ) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, @@ -67,62 +58,57 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - "mode,icon,traffic_mode,unit_system,arrival_time,departure_time,expected_state,expected_distance,expected_duration_in_traffic", + "mode,icon,unit_system,arrival_time,departure_time,expected_duration,expected_distance,expected_duration_in_traffic", [ ( TRAVEL_MODE_CAR, ICON_CAR, - TRAFFIC_MODE_ENABLED, "metric", None, None, + "30", + "23.903", "31", - 23.903, - 31.016666666666666, ), ( TRAVEL_MODE_BICYCLE, ICON_BICYCLE, - TRAFFIC_MODE_DISABLED, "metric", None, None, "30", - 23.903, - 30.05, + "23.903", + "30", ), ( TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, - TRAFFIC_MODE_DISABLED, "imperial", None, None, "30", - 14.852631013, - 30.05, + "14.852631013", + "30", ), ( TRAVEL_MODE_PUBLIC_TIME_TABLE, ICON_PUBLIC, - TRAFFIC_MODE_DISABLED, "imperial", "08:00:00", None, "30", - 14.852631013, - 30.05, + "14.852631013", + "30", ), ( TRAVEL_MODE_TRUCK, ICON_TRUCK, - TRAFFIC_MODE_ENABLED, "metric", None, "08:00:00", + "30", + "23.903", "31", - 23.903, - 31.016666666666666, ), ], ) @@ -131,11 +117,10 @@ async def test_sensor( hass: HomeAssistant, mode, icon, - traffic_mode, unit_system, arrival_time, departure_time, - expected_state, + expected_duration, expected_distance, expected_duration_in_traffic, ): @@ -153,7 +138,6 @@ async def test_sensor( CONF_NAME: "test", }, options={ - CONF_TRAFFIC_MODE: traffic_mode, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, CONF_ARRIVAL_TIME: arrival_time, CONF_DEPARTURE_TIME: departure_time, @@ -166,44 +150,57 @@ async def test_sensor( hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES + duration = hass.states.get("sensor.test_duration") + assert duration.attributes.get("unit_of_measurement") == TIME_MINUTES assert ( - sensor.attributes.get(ATTR_ATTRIBUTION) + duration.attributes.get(ATTR_ATTRIBUTION) == "With the support of HERE Technologies. All information is provided without warranty of any kind." ) - assert sensor.state == expected_state + assert duration.attributes.get(ATTR_ICON) == icon + assert duration.state == expected_duration - assert sensor.attributes.get(ATTR_DURATION) == 30.05 - assert sensor.attributes.get(ATTR_DISTANCE) == expected_distance - assert sensor.attributes.get(ATTR_ROUTE) == ( + assert ( + hass.states.get("sensor.test_duration_in_traffic").state + == expected_duration_in_traffic + ) + assert hass.states.get("sensor.test_distance").state == expected_distance + assert hass.states.get("sensor.test_route").state == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == unit_system assert ( - sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == expected_duration_in_traffic + hass.states.get("sensor.test_duration_in_traffic").state + == expected_duration_in_traffic ) - assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( - [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] + assert hass.states.get("sensor.test_origin").state == "22nd St NW" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == CAR_ORIGIN_LATITUDE ) - assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( - [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE] - ) - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S" - assert sensor.attributes.get(CONF_MODE) == mode - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is ( - traffic_mode == TRAFFIC_MODE_ENABLED + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == CAR_ORIGIN_LONGITUDE ) - assert sensor.attributes.get(ATTR_ICON) == icon + assert hass.states.get("sensor.test_origin").state == "22nd St NW" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == CAR_ORIGIN_LATITUDE + ) + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == CAR_ORIGIN_LONGITUDE + ) - # Test traffic mode disabled for vehicles - if mode in TRAVEL_MODES_VEHICLE: - assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( - ATTR_DURATION_IN_TRAFFIC - ) + assert hass.states.get("sensor.test_destination").state == "Service Rd S" + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LATITUDE) + == CAR_DESTINATION_LATITUDE + ) + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LONGITUDE) + == CAR_DESTINATION_LONGITUDE + ) @pytest.mark.usefixtures("valid_response") @@ -261,7 +258,9 @@ async def test_no_attribution(hass: HomeAssistant): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert hass.states.get("sensor.test").attributes.get(ATTR_ATTRIBUTION) is None + assert ( + hass.states.get("sensor.test_duration").attributes.get(ATTR_ATTRIBUTION) is None + ) async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): @@ -305,8 +304,7 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + assert hass.states.get("sensor.test_distance").state == "23.903" valid_response.assert_called_with( [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE],