mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 03:37:07 +00:00
wsdot component adopts wsdot package (#144914)
* wsdot component adopts wsdot package * update generated files * format code * move wsdot to async_setup_platform * Fix tests * cast wsdot travel id * bump wsdot to 0.0.1 --------- Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
ea9fc6052d
commit
b3ba506e6c
@ -4,5 +4,7 @@
|
|||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/wsdot",
|
"documentation": "https://www.home-assistant.io/integrations/wsdot",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "legacy"
|
"loggers": ["wsdot"],
|
||||||
|
"quality_scale": "legacy",
|
||||||
|
"requirements": ["wsdot==0.0.1"]
|
||||||
}
|
}
|
||||||
|
@ -2,44 +2,32 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import timedelta
|
||||||
from http import HTTPStatus
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from wsdot import TravelTime, WsdotTravelError, WsdotTravelTimes
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_ID, CONF_NAME, UnitOfTime
|
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME, UnitOfTime
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
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.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_ACCESS_CODE = "AccessCode"
|
|
||||||
ATTR_AVG_TIME = "AverageTime"
|
|
||||||
ATTR_CURRENT_TIME = "CurrentTime"
|
|
||||||
ATTR_DESCRIPTION = "Description"
|
|
||||||
ATTR_TIME_UPDATED = "TimeUpdated"
|
|
||||||
ATTR_TRAVEL_TIME_ID = "TravelTimeID"
|
|
||||||
|
|
||||||
ATTRIBUTION = "Data provided by WSDOT"
|
ATTRIBUTION = "Data provided by WSDOT"
|
||||||
|
|
||||||
CONF_TRAVEL_TIMES = "travel_time"
|
CONF_TRAVEL_TIMES = "travel_time"
|
||||||
|
|
||||||
ICON = "mdi:car"
|
ICON = "mdi:car"
|
||||||
|
DOMAIN = "wsdot"
|
||||||
RESOURCE = (
|
|
||||||
"http://www.wsdot.wa.gov/Traffic/api/TravelTimes/"
|
|
||||||
"TravelTimesREST.svc/GetTravelTimeAsJson"
|
|
||||||
)
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=3)
|
SCAN_INTERVAL = timedelta(minutes=3)
|
||||||
|
|
||||||
@ -53,7 +41,7 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
add_entities: AddEntitiesCallback,
|
add_entities: AddEntitiesCallback,
|
||||||
@ -61,12 +49,14 @@ def setup_platform(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the WSDOT sensor."""
|
"""Set up the WSDOT sensor."""
|
||||||
sensors = []
|
sensors = []
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
api_key = config[CONF_API_KEY]
|
||||||
|
wsdot_travel = WsdotTravelTimes(api_key=api_key, session=session)
|
||||||
for travel_time in config[CONF_TRAVEL_TIMES]:
|
for travel_time in config[CONF_TRAVEL_TIMES]:
|
||||||
name = travel_time.get(CONF_NAME) or travel_time.get(CONF_ID)
|
name = travel_time.get(CONF_NAME) or travel_time.get(CONF_ID)
|
||||||
|
travel_time_id = int(travel_time[CONF_ID])
|
||||||
sensors.append(
|
sensors.append(
|
||||||
WashingtonStateTravelTimeSensor(
|
WashingtonStateTravelTimeSensor(name, wsdot_travel, travel_time_id)
|
||||||
name, config[CONF_API_KEY], travel_time.get(CONF_ID)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_entities(sensors, True)
|
add_entities(sensors, True)
|
||||||
@ -82,10 +72,8 @@ class WashingtonStateTransportSensor(SensorEntity):
|
|||||||
|
|
||||||
_attr_icon = ICON
|
_attr_icon = ICON
|
||||||
|
|
||||||
def __init__(self, name: str, access_code: str) -> None:
|
def __init__(self, name: str) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._data: dict[str, str | int | None] = {}
|
|
||||||
self._access_code = access_code
|
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state: int | None = None
|
self._state: int | None = None
|
||||||
|
|
||||||
@ -106,57 +94,28 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
|
|||||||
_attr_attribution = ATTRIBUTION
|
_attr_attribution = ATTRIBUTION
|
||||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||||
|
|
||||||
def __init__(self, name: str, access_code: str, travel_time_id: str) -> None:
|
def __init__(
|
||||||
|
self, name: str, wsdot_travel: WsdotTravelTimes, travel_time_id: int
|
||||||
|
) -> None:
|
||||||
"""Construct a travel time sensor."""
|
"""Construct a travel time sensor."""
|
||||||
|
super().__init__(name)
|
||||||
|
self._data: TravelTime | None = None
|
||||||
self._travel_time_id = travel_time_id
|
self._travel_time_id = travel_time_id
|
||||||
WashingtonStateTransportSensor.__init__(self, name, access_code)
|
self._wsdot_travel = wsdot_travel
|
||||||
|
|
||||||
def update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest data from WSDOT."""
|
"""Get the latest data from WSDOT."""
|
||||||
params = {
|
try:
|
||||||
ATTR_ACCESS_CODE: self._access_code,
|
travel_time = await self._wsdot_travel.get_travel_time(self._travel_time_id)
|
||||||
ATTR_TRAVEL_TIME_ID: self._travel_time_id,
|
except WsdotTravelError:
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.get(RESOURCE, params, timeout=10)
|
|
||||||
if response.status_code != HTTPStatus.OK:
|
|
||||||
_LOGGER.warning("Invalid response from WSDOT API")
|
_LOGGER.warning("Invalid response from WSDOT API")
|
||||||
else:
|
else:
|
||||||
self._data = response.json()
|
self._data = travel_time
|
||||||
_state = self._data.get(ATTR_CURRENT_TIME)
|
self._state = travel_time.CurrentTime
|
||||||
if not isinstance(_state, int):
|
|
||||||
self._state = None
|
|
||||||
else:
|
|
||||||
self._state = _state
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||||
"""Return other details about the sensor state."""
|
"""Return other details about the sensor state."""
|
||||||
if self._data is not None:
|
if self._data is not None:
|
||||||
attrs: dict[str, str | int | None | datetime] = {}
|
return self._data.model_dump()
|
||||||
for key in (
|
|
||||||
ATTR_AVG_TIME,
|
|
||||||
ATTR_NAME,
|
|
||||||
ATTR_DESCRIPTION,
|
|
||||||
ATTR_TRAVEL_TIME_ID,
|
|
||||||
):
|
|
||||||
attrs[key] = self._data.get(key)
|
|
||||||
attrs[ATTR_TIME_UPDATED] = _parse_wsdot_timestamp(
|
|
||||||
self._data.get(ATTR_TIME_UPDATED)
|
|
||||||
)
|
|
||||||
return attrs
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _parse_wsdot_timestamp(timestamp: Any) -> datetime | None:
|
|
||||||
"""Convert WSDOT timestamp to datetime."""
|
|
||||||
if not isinstance(timestamp, str):
|
|
||||||
return None
|
|
||||||
# ex: Date(1485040200000-0800)
|
|
||||||
timestamp_parts = re.search(r"Date\((\d+)([+-]\d\d)\d\d\)", timestamp)
|
|
||||||
if timestamp_parts is None:
|
|
||||||
return None
|
|
||||||
milliseconds, tzone = timestamp_parts.groups()
|
|
||||||
return datetime.fromtimestamp(
|
|
||||||
int(milliseconds) / 1000, tz=timezone(timedelta(hours=int(tzone)))
|
|
||||||
)
|
|
||||||
|
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@ -3097,6 +3097,9 @@ wled==0.21.0
|
|||||||
# homeassistant.components.wolflink
|
# homeassistant.components.wolflink
|
||||||
wolf-comm==0.0.23
|
wolf-comm==0.0.23
|
||||||
|
|
||||||
|
# homeassistant.components.wsdot
|
||||||
|
wsdot==0.0.1
|
||||||
|
|
||||||
# homeassistant.components.wyoming
|
# homeassistant.components.wyoming
|
||||||
wyoming==1.5.4
|
wyoming==1.5.4
|
||||||
|
|
||||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -2505,6 +2505,9 @@ wled==0.21.0
|
|||||||
# homeassistant.components.wolflink
|
# homeassistant.components.wolflink
|
||||||
wolf-comm==0.0.23
|
wolf-comm==0.0.23
|
||||||
|
|
||||||
|
# homeassistant.components.wsdot
|
||||||
|
wsdot==0.0.1
|
||||||
|
|
||||||
# homeassistant.components.wyoming
|
# homeassistant.components.wyoming
|
||||||
wyoming==1.5.4
|
wyoming==1.5.4
|
||||||
|
|
||||||
|
24
tests/components/wsdot/conftest.py
Normal file
24
tests/components/wsdot/conftest.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""Provide common WSDOT fixtures."""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from wsdot import TravelTime
|
||||||
|
|
||||||
|
from homeassistant.components.wsdot.sensor import DOMAIN
|
||||||
|
|
||||||
|
from tests.common import load_json_object_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_travel_time() -> AsyncGenerator[TravelTime]:
|
||||||
|
"""WsdotTravelTimes.get_travel_time is mocked to return a TravelTime data based on test fixture payload."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.wsdot.sensor.WsdotTravelTimes", autospec=True
|
||||||
|
) as mock:
|
||||||
|
client = mock.return_value
|
||||||
|
client.get_travel_time.return_value = TravelTime(
|
||||||
|
**load_json_object_fixture("wsdot.json", DOMAIN)
|
||||||
|
)
|
||||||
|
yield mock
|
@ -1,64 +1,41 @@
|
|||||||
"""The tests for the WSDOT platform."""
|
"""The tests for the WSDOT platform."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
import re
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
import requests_mock
|
|
||||||
|
|
||||||
from homeassistant.components.wsdot import sensor as wsdot
|
|
||||||
from homeassistant.components.wsdot.sensor import (
|
from homeassistant.components.wsdot.sensor import (
|
||||||
ATTR_DESCRIPTION,
|
|
||||||
ATTR_TIME_UPDATED,
|
|
||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_TRAVEL_TIMES,
|
CONF_TRAVEL_TIMES,
|
||||||
RESOURCE,
|
DOMAIN,
|
||||||
SCAN_INTERVAL,
|
|
||||||
)
|
)
|
||||||
|
from homeassistant.const import CONF_PLATFORM
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import load_fixture
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
CONF_API_KEY: "foo",
|
CONF_API_KEY: "foo",
|
||||||
SCAN_INTERVAL: timedelta(seconds=120),
|
|
||||||
CONF_TRAVEL_TIMES: [{CONF_ID: 96, CONF_NAME: "I90 EB"}],
|
CONF_TRAVEL_TIMES: [{CONF_ID: 96, CONF_NAME: "I90 EB"}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_with_config(hass: HomeAssistant) -> None:
|
async def test_setup_with_config(
|
||||||
|
hass: HomeAssistant, mock_travel_time: AsyncMock
|
||||||
|
) -> None:
|
||||||
"""Test the platform setup with configuration."""
|
"""Test the platform setup with configuration."""
|
||||||
assert await async_setup_component(hass, "sensor", {"wsdot": config})
|
assert await async_setup_component(
|
||||||
|
hass, "sensor", {"sensor": [{CONF_PLATFORM: DOMAIN, **config}]}
|
||||||
|
)
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.i90_eb")
|
||||||
async def test_setup(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> None:
|
assert state is not None
|
||||||
"""Test for operational WSDOT sensor with proper attributes."""
|
assert state.name == "I90 EB"
|
||||||
entities = []
|
assert state.state == "11"
|
||||||
|
|
||||||
def add_entities(new_entities, update_before_add=False):
|
|
||||||
"""Mock add entities."""
|
|
||||||
for entity in new_entities:
|
|
||||||
entity.hass = hass
|
|
||||||
|
|
||||||
if update_before_add:
|
|
||||||
for entity in new_entities:
|
|
||||||
entity.update()
|
|
||||||
|
|
||||||
entities.extend(new_entities)
|
|
||||||
|
|
||||||
uri = re.compile(RESOURCE + "*")
|
|
||||||
requests_mock.get(uri, text=load_fixture("wsdot/wsdot.json"))
|
|
||||||
wsdot.setup_platform(hass, config, add_entities)
|
|
||||||
assert len(entities) == 1
|
|
||||||
sensor = entities[0]
|
|
||||||
assert sensor.name == "I90 EB"
|
|
||||||
assert sensor.state == 11
|
|
||||||
assert (
|
assert (
|
||||||
sensor.extra_state_attributes[ATTR_DESCRIPTION]
|
state.attributes["Description"]
|
||||||
== "Downtown Seattle to Downtown Bellevue via I-90"
|
== "Downtown Seattle to Downtown Bellevue via I-90"
|
||||||
)
|
)
|
||||||
assert sensor.extra_state_attributes[ATTR_TIME_UPDATED] == datetime(
|
assert state.attributes["TimeUpdated"] == datetime(
|
||||||
2017, 1, 21, 15, 10, tzinfo=timezone(timedelta(hours=-8))
|
2017, 1, 21, 15, 10, tzinfo=timezone(timedelta(hours=-8))
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user