mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02: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:
@ -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"]
|
||||
}
|
||||
|
@ -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
3
requirements_all.txt
generated
@ -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
|
||||
|
||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -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
|
||||
|
||||
|
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."""
|
||||
|
||||
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))
|
||||
)
|
||||
|
Reference in New Issue
Block a user