Split attributes into sensors for here_travel_time (#72405)

pull/74222/head
Kevin Stillhammer 2022-06-30 07:09:52 +02:00 committed by GitHub
parent 555e9c6762
commit bef512c425
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 187 additions and 120 deletions

View File

@ -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),

View File

@ -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"

View File

@ -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

View File

@ -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],