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:
Jeremiah Paige
2025-05-21 11:15:26 -07:00
committed by GitHub
parent ea9fc6052d
commit b3ba506e6c
6 changed files with 73 additions and 105 deletions

View File

@ -4,5 +4,7 @@
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/wsdot",
"iot_class": "cloud_polling",
"quality_scale": "legacy"
"loggers": ["wsdot"],
"quality_scale": "legacy",
"requirements": ["wsdot==0.0.1"]
}

View File

@ -2,44 +2,32 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from http import HTTPStatus
from datetime import timedelta
import logging
import re
from typing import Any
import requests
import voluptuous as vol
from wsdot import TravelTime, WsdotTravelError, WsdotTravelTimes
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
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.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.typing import ConfigType, DiscoveryInfoType
_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"
CONF_TRAVEL_TIMES = "travel_time"
ICON = "mdi:car"
RESOURCE = (
"http://www.wsdot.wa.gov/Traffic/api/TravelTimes/"
"TravelTimesREST.svc/GetTravelTimeAsJson"
)
DOMAIN = "wsdot"
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,
config: ConfigType,
add_entities: AddEntitiesCallback,
@ -61,12 +49,14 @@ def setup_platform(
) -> None:
"""Set up the WSDOT sensor."""
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]:
name = travel_time.get(CONF_NAME) or travel_time.get(CONF_ID)
travel_time_id = int(travel_time[CONF_ID])
sensors.append(
WashingtonStateTravelTimeSensor(
name, config[CONF_API_KEY], travel_time.get(CONF_ID)
)
WashingtonStateTravelTimeSensor(name, wsdot_travel, travel_time_id)
)
add_entities(sensors, True)
@ -82,10 +72,8 @@ class WashingtonStateTransportSensor(SensorEntity):
_attr_icon = ICON
def __init__(self, name: str, access_code: str) -> None:
def __init__(self, name: str) -> None:
"""Initialize the sensor."""
self._data: dict[str, str | int | None] = {}
self._access_code = access_code
self._name = name
self._state: int | None = None
@ -106,57 +94,28 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
_attr_attribution = ATTRIBUTION
_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."""
super().__init__(name)
self._data: TravelTime | None = None
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."""
params = {
ATTR_ACCESS_CODE: self._access_code,
ATTR_TRAVEL_TIME_ID: self._travel_time_id,
}
response = requests.get(RESOURCE, params, timeout=10)
if response.status_code != HTTPStatus.OK:
try:
travel_time = await self._wsdot_travel.get_travel_time(self._travel_time_id)
except WsdotTravelError:
_LOGGER.warning("Invalid response from WSDOT API")
else:
self._data = response.json()
_state = self._data.get(ATTR_CURRENT_TIME)
if not isinstance(_state, int):
self._state = None
else:
self._state = _state
self._data = travel_time
self._state = travel_time.CurrentTime
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return other details about the sensor state."""
if self._data is not None:
attrs: dict[str, str | int | None | datetime] = {}
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 self._data.model_dump()
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
View File

@ -3097,6 +3097,9 @@ wled==0.21.0
# homeassistant.components.wolflink
wolf-comm==0.0.23
# homeassistant.components.wsdot
wsdot==0.0.1
# homeassistant.components.wyoming
wyoming==1.5.4

View File

@ -2505,6 +2505,9 @@ wled==0.21.0
# homeassistant.components.wolflink
wolf-comm==0.0.23
# homeassistant.components.wsdot
wsdot==0.0.1
# homeassistant.components.wyoming
wyoming==1.5.4

View 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

View File

@ -1,64 +1,41 @@
"""The tests for the WSDOT platform."""
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 (
ATTR_DESCRIPTION,
ATTR_TIME_UPDATED,
CONF_API_KEY,
CONF_ID,
CONF_NAME,
CONF_TRAVEL_TIMES,
RESOURCE,
SCAN_INTERVAL,
DOMAIN,
)
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import load_fixture
config = {
CONF_API_KEY: "foo",
SCAN_INTERVAL: timedelta(seconds=120),
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."""
assert await async_setup_component(hass, "sensor", {"wsdot": config})
assert await async_setup_component(
hass, "sensor", {"sensor": [{CONF_PLATFORM: DOMAIN, **config}]}
)
async def test_setup(hass: HomeAssistant, requests_mock: requests_mock.Mocker) -> None:
"""Test for operational WSDOT sensor with proper attributes."""
entities = []
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
state = hass.states.get("sensor.i90_eb")
assert state is not None
assert state.name == "I90 EB"
assert state.state == "11"
assert (
sensor.extra_state_attributes[ATTR_DESCRIPTION]
state.attributes["Description"]
== "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))
)