mirror of
https://github.com/home-assistant/core.git
synced 2025-12-30 03:38:56 +00:00
Compare commits
4 Commits
tibber_dat
...
2026.1.0b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f2b1f0eff | ||
|
|
a1a1d65ee4 | ||
|
|
8778d4c704 | ||
|
|
7790a2ebdd |
@@ -2,16 +2,25 @@
|
||||
|
||||
from pyblu import Player
|
||||
from pyblu.errors import PlayerUnreachableError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import (
|
||||
ATTR_MASTER,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_SET_TIMER,
|
||||
SERVICE_UNJOIN,
|
||||
)
|
||||
from .coordinator import (
|
||||
BluesoundConfigEntry,
|
||||
BluesoundCoordinator,
|
||||
@@ -28,6 +37,38 @@ PLATFORMS = [
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Bluesound."""
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SET_TIMER,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema=None,
|
||||
func="async_increase_timer",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema=None,
|
||||
func="async_clear_timer",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_JOIN,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema={vol.Required(ATTR_MASTER): cv.entity_id},
|
||||
func="async_bluesound_join",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_UNJOIN,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema=None,
|
||||
func="async_bluesound_unjoin",
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -4,3 +4,8 @@ DOMAIN = "bluesound"
|
||||
INTEGRATION_TITLE = "Bluesound"
|
||||
ATTR_BLUESOUND_GROUP = "bluesound_group"
|
||||
ATTR_MASTER = "master"
|
||||
|
||||
SERVICE_CLEAR_TIMER = "clear_sleep_timer"
|
||||
SERVICE_JOIN = "join"
|
||||
SERVICE_SET_TIMER = "set_sleep_timer"
|
||||
SERVICE_UNJOIN = "unjoin"
|
||||
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyblu import Input, Player, Preset, Status, SyncStatus
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -22,12 +21,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_platform,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
@@ -41,7 +35,15 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
|
||||
from .const import (
|
||||
ATTR_BLUESOUND_GROUP,
|
||||
ATTR_MASTER,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_TIMER,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_SET_TIMER,
|
||||
SERVICE_UNJOIN,
|
||||
)
|
||||
from .coordinator import BluesoundCoordinator
|
||||
from .utils import (
|
||||
dispatcher_join_signal,
|
||||
@@ -60,11 +62,6 @@ SCAN_INTERVAL = timedelta(minutes=15)
|
||||
DATA_BLUESOUND = DOMAIN
|
||||
DEFAULT_PORT = 11000
|
||||
|
||||
SERVICE_CLEAR_TIMER = "clear_sleep_timer"
|
||||
SERVICE_JOIN = "join"
|
||||
SERVICE_SET_TIMER = "set_sleep_timer"
|
||||
SERVICE_UNJOIN = "unjoin"
|
||||
|
||||
POLL_TIMEOUT = 120
|
||||
|
||||
|
||||
@@ -81,20 +78,6 @@ async def async_setup_entry(
|
||||
config_entry.runtime_data.player,
|
||||
)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_TIMER, None, "async_increase_timer"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_CLEAR_TIMER, None, "async_clear_timer"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_JOIN, {vol.Required(ATTR_MASTER): cv.entity_id}, "async_bluesound_join"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_UNJOIN, None, "async_bluesound_unjoin"
|
||||
)
|
||||
|
||||
async_add_entities([bluesound_player], update_before_add=True)
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item,
|
||||
percentage_to_ranged_value,
|
||||
ranged_value_to_percentage,
|
||||
)
|
||||
@@ -22,6 +24,9 @@ from .entity import SmartThingsEntity
|
||||
|
||||
SPEED_RANGE = (1, 3) # off is not included
|
||||
|
||||
SMART = 14
|
||||
PRESET_SMART = "smart"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -30,7 +35,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add fans for a config entry."""
|
||||
entry_data = entry.runtime_data
|
||||
async_add_entities(
|
||||
entities: list[FanEntity] = [
|
||||
SmartThingsFan(entry_data.client, device)
|
||||
for device in entry_data.devices.values()
|
||||
if Capability.SWITCH in device.status[MAIN]
|
||||
@@ -42,7 +47,20 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
and Capability.THERMOSTAT_COOLING_SETPOINT not in device.status[MAIN]
|
||||
]
|
||||
entities.extend(
|
||||
SmartThingsHood(entry_data.client, device)
|
||||
for device in entry_data.devices.values()
|
||||
if Capability.SWITCH in device.status[MAIN]
|
||||
and Capability.SAMSUNG_CE_HOOD_FAN_SPEED in device.status[MAIN]
|
||||
and (
|
||||
device.status[MAIN][Capability.SAMSUNG_CE_HOOD_FAN_SPEED][
|
||||
Attribute.SETTABLE_MIN_FAN_SPEED
|
||||
].value
|
||||
== SMART
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class SmartThingsFan(SmartThingsEntity, FanEntity):
|
||||
@@ -149,3 +167,103 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
|
||||
return self.get_attribute_value(
|
||||
Capability.AIR_CONDITIONER_FAN_MODE, Attribute.SUPPORTED_AC_FAN_MODES
|
||||
)
|
||||
|
||||
|
||||
class SmartThingsHood(SmartThingsEntity, FanEntity):
|
||||
"""Define a SmartThings Hood."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_features = (
|
||||
FanEntityFeature.TURN_ON
|
||||
| FanEntityFeature.TURN_OFF
|
||||
| FanEntityFeature.PRESET_MODE
|
||||
| FanEntityFeature.SET_SPEED
|
||||
)
|
||||
_attr_preset_modes = [PRESET_SMART]
|
||||
_attr_translation_key = "hood"
|
||||
|
||||
def __init__(self, client: SmartThings, device: FullDevice) -> None:
|
||||
"""Init the class."""
|
||||
super().__init__(
|
||||
client,
|
||||
device,
|
||||
{
|
||||
Capability.SWITCH,
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def fan_speeds(self) -> list[int]:
|
||||
"""Return a list of available fan speeds."""
|
||||
return [
|
||||
speed
|
||||
for speed in self.get_attribute_value(
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED, Attribute.SUPPORTED_HOOD_FAN_SPEED
|
||||
)
|
||||
if speed != SMART
|
||||
]
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset_mode of the fan."""
|
||||
await self.execute_device_command(
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
|
||||
Command.SET_HOOD_FAN_SPEED,
|
||||
argument=SMART,
|
||||
)
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed percentage of the fan."""
|
||||
if percentage == 0:
|
||||
await self.execute_device_command(Capability.SWITCH, Command.OFF)
|
||||
else:
|
||||
await self.execute_device_command(
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
|
||||
Command.SET_HOOD_FAN_SPEED,
|
||||
argument=percentage_to_ordered_list_item(self.fan_speeds, percentage),
|
||||
)
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn the fan on."""
|
||||
await self.execute_device_command(Capability.SWITCH, Command.ON)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the fan off."""
|
||||
await self.execute_device_command(Capability.SWITCH, Command.OFF)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if fan is on."""
|
||||
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on"
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
if (
|
||||
self.get_attribute_value(
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED, Attribute.HOOD_FAN_SPEED
|
||||
)
|
||||
== SMART
|
||||
):
|
||||
return PRESET_SMART
|
||||
return None
|
||||
|
||||
@property
|
||||
def percentage(self) -> int | None:
|
||||
"""Return the current speed percentage."""
|
||||
fan_speed = self.get_attribute_value(
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED, Attribute.HOOD_FAN_SPEED
|
||||
)
|
||||
if fan_speed == SMART:
|
||||
return None
|
||||
return ordered_list_item_to_percentage(self.fan_speeds, fan_speed)
|
||||
|
||||
@property
|
||||
def speed_count(self) -> int:
|
||||
"""Return the number of available speeds."""
|
||||
return len(self.fan_speeds)
|
||||
|
||||
@@ -53,6 +53,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"hood": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"smart": "mdi:brain"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"freezer_temperature": {
|
||||
"default": "mdi:snowflake-thermometer"
|
||||
|
||||
@@ -138,6 +138,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fan": {
|
||||
"hood": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"smart": "Smart"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"cool_select_plus_temperature": {
|
||||
"name": "CoolSelect+ temperature"
|
||||
|
||||
@@ -1 +1,36 @@
|
||||
"""The wsdot component."""
|
||||
|
||||
import wsdot as wsdot_api
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
type WsdotConfigEntry = ConfigEntry[wsdot_api.WsdotTravelTimes]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: WsdotConfigEntry) -> bool:
|
||||
"""Set up wsdot as config entry."""
|
||||
session = async_get_clientsession(hass)
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
wsdot_travel_times = wsdot_api.WsdotTravelTimes(api_key=api_key, session=session)
|
||||
try:
|
||||
# The only way to validate the provided API key is to request data
|
||||
# we don't need the data here, only the non-exception
|
||||
await wsdot_travel_times.get_all_travel_times()
|
||||
except wsdot_api.WsdotTravelError as api_error:
|
||||
raise ConfigEntryError("Bad auth") from api_error
|
||||
entry.runtime_data = wsdot_travel_times
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
157
homeassistant/components/wsdot/config_flow.py
Normal file
157
homeassistant/components/wsdot/config_flow.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""Adds config flow for wsdot."""
|
||||
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
import wsdot as wsdot_api
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_USER,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentry,
|
||||
ConfigSubentryFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
|
||||
from .const import CONF_TRAVEL_TIMES, DOMAIN, SUBENTRY_TRAVEL_TIMES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WSDOTConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for WSDOT."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
data = {CONF_API_KEY: user_input[CONF_API_KEY]}
|
||||
self._async_abort_entries_match(data)
|
||||
wsdot_travel_times = wsdot_api.WsdotTravelTimes(user_input[CONF_API_KEY])
|
||||
try:
|
||||
await wsdot_travel_times.get_all_travel_times()
|
||||
except wsdot_api.WsdotTravelError as ws_error:
|
||||
if ws_error.status == 400:
|
||||
errors[CONF_API_KEY] = "invalid_api_key"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=DOMAIN,
|
||||
data=data,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id=SOURCE_USER,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by import."""
|
||||
self._async_abort_entries_match({CONF_API_KEY: import_info[CONF_API_KEY]})
|
||||
wsdot_travel_times = wsdot_api.WsdotTravelTimes(import_info[CONF_API_KEY])
|
||||
try:
|
||||
travel_time_routes = await wsdot_travel_times.get_all_travel_times()
|
||||
except wsdot_api.WsdotTravelError as ws_error:
|
||||
if ws_error.status == 400:
|
||||
return self.async_abort(reason="invalid_api_key")
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
subentries = []
|
||||
for route in import_info[CONF_TRAVEL_TIMES]:
|
||||
maybe_travel_time = [
|
||||
tt
|
||||
for tt in travel_time_routes
|
||||
# old platform configs could store the id as either a str or an int
|
||||
if str(tt.TravelTimeID) == str(route[CONF_ID])
|
||||
]
|
||||
if not maybe_travel_time:
|
||||
return self.async_abort(reason="invalid_travel_time_id")
|
||||
travel_time = maybe_travel_time[0]
|
||||
route_name = travel_time.Name
|
||||
unique_id = "_".join(travel_time.Name.split())
|
||||
subentries.append(
|
||||
ConfigSubentry(
|
||||
subentry_type=SUBENTRY_TRAVEL_TIMES,
|
||||
unique_id=unique_id,
|
||||
title=route_name,
|
||||
data=MappingProxyType(
|
||||
{CONF_NAME: travel_time.Name, CONF_ID: travel_time.TravelTimeID}
|
||||
),
|
||||
).as_dict()
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=DOMAIN,
|
||||
data={
|
||||
CONF_API_KEY: import_info[CONF_API_KEY],
|
||||
},
|
||||
subentries=subentries,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by wsdot."""
|
||||
return {SUBENTRY_TRAVEL_TIMES: TravelTimeSubentryFlowHandler}
|
||||
|
||||
|
||||
class TravelTimeSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Handle subentry flow for adding WSDOT Travel Times."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
"""Initialize TravelTimeSubentryFlowHandler."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.travel_times: dict[str, int] | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Add a new Travel Time subentry."""
|
||||
if self.travel_times is None:
|
||||
client = self._get_entry().runtime_data
|
||||
travel_times = await client.get_all_travel_times()
|
||||
self.travel_times = {tt.Name: tt.TravelTimeID for tt in travel_times}
|
||||
|
||||
if user_input is not None:
|
||||
name = user_input[CONF_NAME]
|
||||
tt_id = self.travel_times[name]
|
||||
unique_id = str(tt_id)
|
||||
data = {CONF_NAME: name, CONF_ID: tt_id}
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
for subentry in entry.subentries.values():
|
||||
if subentry.unique_id == unique_id:
|
||||
return self.async_abort(reason="already_configured")
|
||||
return self.async_create_entry(title=name, unique_id=unique_id, data=data)
|
||||
|
||||
names = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(self.travel_times.keys()),
|
||||
sort=True,
|
||||
)
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id=SOURCE_USER,
|
||||
data_schema=vol.Schema({vol.Required(CONF_NAME): names}),
|
||||
errors={},
|
||||
)
|
||||
11
homeassistant/components/wsdot/const.py
Normal file
11
homeassistant/components/wsdot/const.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Constants for wsdot component."""
|
||||
|
||||
ATTRIBUTION = "Data provided by WSDOT"
|
||||
|
||||
CONF_DATA = "data"
|
||||
CONF_TITLE = "title"
|
||||
CONF_TRAVEL_TIMES = "travel_time"
|
||||
|
||||
DOMAIN = "wsdot"
|
||||
|
||||
SUBENTRY_TRAVEL_TIMES = "travel_time"
|
||||
@@ -2,7 +2,9 @@
|
||||
"domain": "wsdot",
|
||||
"name": "Washington State Department of Transportation (WSDOT)",
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/wsdot",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["wsdot"],
|
||||
"quality_scale": "legacy",
|
||||
|
||||
@@ -7,27 +7,29 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from wsdot import TravelTime, WsdotTravelError, WsdotTravelTimes
|
||||
import wsdot as wsdot_api
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
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.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import WsdotConfigEntry
|
||||
from .const import ATTRIBUTION, CONF_TRAVEL_TIMES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = "Data provided by WSDOT"
|
||||
|
||||
CONF_TRAVEL_TIMES = "travel_time"
|
||||
|
||||
ICON = "mdi:car"
|
||||
DOMAIN = "wsdot"
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=3)
|
||||
|
||||
@@ -44,22 +46,61 @@ PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> 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, wsdot_travel, travel_time_id)
|
||||
"""Migrate a platform-style wsdot to entry-style."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
if (
|
||||
result.get("type") is FlowResultType.ABORT
|
||||
and result.get("reason") != "already_configured"
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
breaks_in_ha_version="2026.7.0",
|
||||
is_fixable=False,
|
||||
is_persistent=True,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
translation_placeholders={"domain": DOMAIN, "integration_title": "WSDOT"},
|
||||
)
|
||||
return
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
"deprecated_yaml",
|
||||
breaks_in_ha_version="2026.7.0",
|
||||
is_fixable=False,
|
||||
is_persistent=True,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={"domain": DOMAIN, "integration_title": "WSDOT"},
|
||||
)
|
||||
|
||||
add_entities(sensors, True)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: WsdotConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the WSDOT sensor."""
|
||||
for subentry_id, subentry in entry.subentries.items():
|
||||
name = subentry.data[CONF_NAME]
|
||||
travel_time_id = subentry.data[CONF_ID]
|
||||
sensor = WashingtonStateTravelTimeSensor(
|
||||
name, entry.runtime_data, travel_time_id
|
||||
)
|
||||
async_add_entities(
|
||||
[sensor], config_subentry_id=subentry_id, update_before_add=True
|
||||
)
|
||||
|
||||
|
||||
class WashingtonStateTransportSensor(SensorEntity):
|
||||
@@ -70,6 +111,7 @@ class WashingtonStateTransportSensor(SensorEntity):
|
||||
can read them and make them available.
|
||||
"""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_icon = ICON
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
@@ -91,23 +133,23 @@ class WashingtonStateTransportSensor(SensorEntity):
|
||||
class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor):
|
||||
"""Travel time sensor from WSDOT."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
|
||||
def __init__(
|
||||
self, name: str, wsdot_travel: WsdotTravelTimes, travel_time_id: int
|
||||
self, name: str, wsdot_travel: wsdot_api.WsdotTravelTimes, travel_time_id: int
|
||||
) -> None:
|
||||
"""Construct a travel time sensor."""
|
||||
super().__init__(name)
|
||||
self._data: TravelTime | None = None
|
||||
self._data: wsdot_api.TravelTime | None = None
|
||||
self._travel_time_id = travel_time_id
|
||||
self._wsdot_travel = wsdot_travel
|
||||
self._attr_unique_id = f"travel_time-{travel_time_id}"
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data from WSDOT."""
|
||||
try:
|
||||
travel_time = await self._wsdot_travel.get_travel_time(self._travel_time_id)
|
||||
except WsdotTravelError:
|
||||
except wsdot_api.WsdotTravelError:
|
||||
_LOGGER.warning("Invalid response from WSDOT API")
|
||||
else:
|
||||
self._data = travel_time
|
||||
|
||||
63
homeassistant/components/wsdot/strings.json
Normal file
63
homeassistant/components/wsdot/strings.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"invalid_travel_time_id": "WSDOT TravelTimeId does not describe a valid travel time route {travel_time_id}",
|
||||
"no_travel_time": "Travel time route created without a travel time entity"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "Your WSDOT API key."
|
||||
},
|
||||
"description": "Set up your Washington State Department of Transportation integration.",
|
||||
"title": "WSDOT setup"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config_subentries": {
|
||||
"travel_time": {
|
||||
"entry_type": "Travel time",
|
||||
"initiate_flow": {
|
||||
"user": "Travel time"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Travel Route Description"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "The official WSDOT description of the route."
|
||||
},
|
||||
"description": "Select one of the provided the highway routes to monitor estimated driving time along that route",
|
||||
"title": "WSDOT travel time setup"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring {domain} using YAML sensor platform is deprecated.\n\nWhile importing your configuration, Home Assistant could not connect to the {domain} API. Please check your internet connection and the status of the {domain} API, then restart Home Assistant to try again, or remove the existing YAML configuration and set the integration up via the UI.",
|
||||
"title": "[%key:component::wsdot::issues::deprecated_yaml_import_issue_invalid_auth::title%]"
|
||||
},
|
||||
"deprecated_yaml_import_issue_invalid_auth": {
|
||||
"description": "Configuring {domain} using YAML sensor platform is deprecated.\n\nWhile importing your configuration, an invalid API key was found. Please update your YAML configuration, or remove the existing YAML configuration and set the integration up via the UI.",
|
||||
"title": "{domain} YAML configuration deprecated"
|
||||
},
|
||||
"deprecated_yaml_import_issue_invalid_travel_time_id": {
|
||||
"description": "Configuring {domain} using YAML sensor platform is deprecated.\n\nWhile importing your configuration, an invalid travel_time_id was found. Please update your YAML configuration, or remove the existing YAML configuration and set the integration up via the UI.",
|
||||
"title": "[%key:component::wsdot::issues::deprecated_yaml_import_issue_invalid_auth::title%]"
|
||||
},
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"description": "Configuring {domain} using YAML sensor platform is deprecated.\n\nWhile importing your configuration, an unknown error occurred. Please restart Home Assistant to try again, or remove the existing YAML configuration and set the integration up via the UI.",
|
||||
"title": "[%key:component::wsdot::issues::deprecated_yaml_import_issue_invalid_auth::title%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 1
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
PATCH_VERSION: Final = "0b0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -776,6 +776,7 @@ FLOWS = {
|
||||
"workday",
|
||||
"worldclock",
|
||||
"ws66i",
|
||||
"wsdot",
|
||||
"wyoming",
|
||||
"xbox",
|
||||
"xiaomi_aqara",
|
||||
|
||||
@@ -7638,8 +7638,8 @@
|
||||
},
|
||||
"wsdot": {
|
||||
"name": "Washington State Department of Transportation (WSDOT)",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"wyoming": {
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.1.0.dev0"
|
||||
version = "2026.1.0b0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"switch": {
|
||||
"switch": {
|
||||
"value": "off",
|
||||
"value": "on",
|
||||
"timestamp": "2025-11-11T23:41:24.907Z"
|
||||
}
|
||||
},
|
||||
@@ -305,7 +305,7 @@
|
||||
"timestamp": "2025-11-11T23:41:21.525Z"
|
||||
},
|
||||
"hoodFanSpeed": {
|
||||
"value": 14,
|
||||
"value": 15,
|
||||
"timestamp": "2025-11-11T23:41:21.525Z"
|
||||
},
|
||||
"supportedHoodFanSpeed": {
|
||||
|
||||
@@ -1,4 +1,63 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[da_ks_hood_01001][fan.range_hood-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': list([
|
||||
'smart',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.range_hood',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <FanEntityFeature: 57>,
|
||||
'translation_key': 'hood',
|
||||
'unique_id': 'fa5fca25-fa7a-1807-030a-2f72ee0f7bff_main',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ks_hood_01001][fan.range_hood-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Range hood',
|
||||
'percentage': 25,
|
||||
'percentage_step': 25.0,
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
'smart',
|
||||
]),
|
||||
'supported_features': <FanEntityFeature: 57>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.range_hood',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[fake_fan][fan.fake_fan-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_ks_walloven_0107x][switch.four_sabbath_mode-entry]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from pysmartthings import Capability, Command
|
||||
from pysmartthings import Attribute, Capability, Command
|
||||
from pysmartthings.models import HealthStatus
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -26,7 +26,12 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration, snapshot_smartthings_entities, trigger_health_update
|
||||
from . import (
|
||||
setup_integration,
|
||||
snapshot_smartthings_entities,
|
||||
trigger_health_update,
|
||||
trigger_update,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -44,7 +49,13 @@ async def test_all_entities(
|
||||
snapshot_smartthings_entities(hass, entity_registry, snapshot, Platform.FAN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["fake_fan"])
|
||||
@pytest.mark.parametrize(
|
||||
("device_fixture", "entity_id", "device_id"),
|
||||
[
|
||||
("fake_fan", "fan.fake_fan", "f1af21a2-d5a1-437c-b10a-b34a87394b71"),
|
||||
("da_ks_hood_01001", "fan.range_hood", "fa5fca25-fa7a-1807-030a-2f72ee0f7bff"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("action", "command"),
|
||||
[
|
||||
@@ -58,6 +69,8 @@ async def test_turn_on_off(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
action: str,
|
||||
command: Command,
|
||||
entity_id: str,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Test turning on and off."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
@@ -65,11 +78,11 @@ async def test_turn_on_off(
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
action,
|
||||
{ATTR_ENTITY_ID: "fan.fake_fan"},
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
devices.execute_device_command.assert_called_once_with(
|
||||
"f1af21a2-d5a1-437c-b10a-b34a87394b71",
|
||||
device_id,
|
||||
Capability.SWITCH,
|
||||
command,
|
||||
MAIN,
|
||||
@@ -100,11 +113,19 @@ async def test_set_percentage(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["fake_fan"])
|
||||
@pytest.mark.parametrize(
|
||||
("device_fixture", "entity_id", "device_id"),
|
||||
[
|
||||
("fake_fan", "fan.fake_fan", "f1af21a2-d5a1-437c-b10a-b34a87394b71"),
|
||||
("da_ks_hood_01001", "fan.range_hood", "fa5fca25-fa7a-1807-030a-2f72ee0f7bff"),
|
||||
],
|
||||
)
|
||||
async def test_set_percentage_off(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Test setting the speed percentage of the fan."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
@@ -112,11 +133,11 @@ async def test_set_percentage_off(
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
{ATTR_ENTITY_ID: "fan.fake_fan", ATTR_PERCENTAGE: 0},
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 0},
|
||||
blocking=True,
|
||||
)
|
||||
devices.execute_device_command.assert_called_once_with(
|
||||
"f1af21a2-d5a1-437c-b10a-b34a87394b71",
|
||||
device_id,
|
||||
Capability.SWITCH,
|
||||
Command.OFF,
|
||||
MAIN,
|
||||
@@ -204,3 +225,80 @@ async def test_availability_at_start(
|
||||
"""Test unavailable at boot."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert hass.states.get("fan.fake_fan").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
|
||||
async def test_set_hood_percentage(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setting the speed percentage of the hood."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
{ATTR_ENTITY_ID: "fan.range_hood", ATTR_PERCENTAGE: 50},
|
||||
blocking=True,
|
||||
)
|
||||
devices.execute_device_command.assert_called_once_with(
|
||||
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
|
||||
Command.SET_HOOD_FAN_SPEED,
|
||||
MAIN,
|
||||
argument=16,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
|
||||
async def test_set_hood_preset_mode(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test setting the preset mode of the hood."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{ATTR_ENTITY_ID: "fan.range_hood", ATTR_PRESET_MODE: "smart"},
|
||||
blocking=True,
|
||||
)
|
||||
devices.execute_device_command.assert_called_once_with(
|
||||
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
|
||||
Command.SET_HOOD_FAN_SPEED,
|
||||
MAIN,
|
||||
argument=14,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", ["da_ks_hood_01001"])
|
||||
async def test_updating_hood_preset_mode(
|
||||
hass: HomeAssistant,
|
||||
devices: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test updating the preset mode of the hood."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get("fan.range_hood")
|
||||
assert state
|
||||
assert state.attributes[ATTR_PRESET_MODE] is None
|
||||
assert state.attributes[ATTR_PERCENTAGE] == 25
|
||||
|
||||
await trigger_update(
|
||||
hass,
|
||||
devices,
|
||||
"fa5fca25-fa7a-1807-030a-2f72ee0f7bff",
|
||||
Capability.SAMSUNG_CE_HOOD_FAN_SPEED,
|
||||
Attribute.HOOD_FAN_SPEED,
|
||||
14,
|
||||
)
|
||||
|
||||
state = hass.states.get("fan.range_hood")
|
||||
assert state
|
||||
assert state.attributes[ATTR_PRESET_MODE] == "smart"
|
||||
assert state.attributes[ATTR_PERCENTAGE] is None
|
||||
|
||||
@@ -1,24 +1,113 @@
|
||||
"""Provide common WSDOT fixtures."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import patch
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from wsdot import TravelTime
|
||||
from wsdot import TravelTime, WsdotTravelError
|
||||
|
||||
from homeassistant.components.wsdot.sensor import DOMAIN
|
||||
from homeassistant.components.wsdot.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigSubentryData
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import load_json_object_fixture
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_travel_time() -> AsyncGenerator[TravelTime]:
|
||||
def mock_travel_time() -> Generator[AsyncMock]:
|
||||
"""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:
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wsdot.wsdot_api.WsdotTravelTimes", autospec=True
|
||||
) as mock,
|
||||
patch(
|
||||
"homeassistant.components.wsdot.config_flow.wsdot_api.WsdotTravelTimes",
|
||||
new=mock,
|
||||
),
|
||||
):
|
||||
client = mock.return_value
|
||||
client.get_travel_time.return_value = TravelTime(
|
||||
**load_json_object_fixture("wsdot.json", DOMAIN)
|
||||
response = TravelTime(**load_json_object_fixture("wsdot.json", DOMAIN))
|
||||
client.get_travel_time.return_value = response
|
||||
client.get_all_travel_times.return_value = [response]
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def failed_travel_time_status() -> int:
|
||||
"""Return the default status code for failed travel time requests."""
|
||||
return 400
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_failed_travel_time(
|
||||
mock_travel_time: AsyncMock, failed_travel_time_status: int
|
||||
) -> AsyncMock:
|
||||
"""WsdotTravelTimes.get_travel_time is mocked to raise a WsdotTravelError."""
|
||||
mock_travel_time.get_travel_time.side_effect = WsdotTravelError(
|
||||
status=failed_travel_time_status
|
||||
)
|
||||
mock_travel_time.get_all_travel_times.side_effect = WsdotTravelError(
|
||||
status=failed_travel_time_status
|
||||
)
|
||||
return mock_travel_time
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_data() -> dict[str, Any]:
|
||||
"""Return valid test config data."""
|
||||
return {
|
||||
CONF_API_KEY: "abcd-1234",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_subentries() -> list[ConfigSubentryData]:
|
||||
"""Mock subentries."""
|
||||
return [
|
||||
ConfigSubentryData(
|
||||
subentry_type="travel_time",
|
||||
title="I-90 EB",
|
||||
unique_id="96",
|
||||
data={
|
||||
CONF_ID: 96,
|
||||
CONF_NAME: "Seattle-Bellevue via I-90 (EB AM)",
|
||||
},
|
||||
)
|
||||
yield mock
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(
|
||||
mock_config_data: dict[str, Any], mock_subentries: list[ConfigSubentryData]
|
||||
) -> MockConfigEntry:
|
||||
"""Mock a wsdot config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=mock_config_data,
|
||||
subentries_data=mock_subentries,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up wsdot integration with subentries for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Mock config entry setup."""
|
||||
with patch(
|
||||
"homeassistant.components.wsdot.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
75
tests/components/wsdot/snapshots/test_sensor.ambr
Normal file
75
tests/components/wsdot/snapshots/test_sensor.ambr
Normal file
@@ -0,0 +1,75 @@
|
||||
# serializer version: 1
|
||||
# name: test_travel_sensor_details[sensor.seattle_bellevue_via_i_90_eb_am-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.seattle_bellevue_via_i_90_eb_am',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:car',
|
||||
'original_name': 'Seattle-Bellevue via I-90 (EB AM)',
|
||||
'platform': 'wsdot',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'travel_time-96',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_travel_sensor_details[sensor.seattle_bellevue_via_i_90_eb_am-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'AverageTime': 11,
|
||||
'CurrentTime': 11,
|
||||
'Description': 'Downtown Seattle to Downtown Bellevue via I-90',
|
||||
'Distance': 10.6,
|
||||
'EndPoint': dict({
|
||||
'Description': 'I-405 @ NE 8th St in Bellevue',
|
||||
'Direction': 'N',
|
||||
'Latitude': 47.61361,
|
||||
'Longitude': -122.18797,
|
||||
'MilePost': 13.6,
|
||||
'RoadName': 'I-405',
|
||||
}),
|
||||
'Name': 'Seattle-Bellevue via I-90 (EB AM)',
|
||||
'StartPoint': dict({
|
||||
'Description': 'I-5 @ University St in Seattle',
|
||||
'Direction': 'S',
|
||||
'Latitude': 47.609294,
|
||||
'Longitude': -122.331759,
|
||||
'MilePost': 165.83,
|
||||
'RoadName': 'I-5',
|
||||
}),
|
||||
'TimeUpdated': datetime.datetime(2017, 1, 21, 15, 10, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=57600))),
|
||||
'TravelTimeID': 96,
|
||||
'attribution': 'Data provided by WSDOT',
|
||||
'friendly_name': 'Seattle-Bellevue via I-90 (EB AM)',
|
||||
'icon': 'mdi:car',
|
||||
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.seattle_bellevue_via_i_90_eb_am',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '11',
|
||||
})
|
||||
# ---
|
||||
282
tests/components/wsdot/test_config_flow.py
Normal file
282
tests/components/wsdot/test_config_flow.py
Normal file
@@ -0,0 +1,282 @@
|
||||
"""Define tests for the wsdot config flow."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from wsdot import WsdotTravelError
|
||||
|
||||
from homeassistant.components.wsdot.const import (
|
||||
CONF_TRAVEL_TIMES,
|
||||
DOMAIN,
|
||||
SUBENTRY_TRAVEL_TIMES,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
VALID_USER_CONFIG = {
|
||||
CONF_API_KEY: "abcd-1234",
|
||||
}
|
||||
|
||||
VALID_USER_TRAVEL_TIME_CONFIG = {
|
||||
CONF_NAME: "Seattle-Bellevue via I-90 (EB AM)",
|
||||
}
|
||||
|
||||
|
||||
async def test_create_user_entry(
|
||||
hass: HomeAssistant, mock_travel_time: AsyncMock
|
||||
) -> None:
|
||||
"""Test that the user step works."""
|
||||
# No user data; form is being show for the first time
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# User data; the user entered data and hit submit
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=VALID_USER_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == DOMAIN
|
||||
assert result["data"][CONF_API_KEY] == "abcd-1234"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("failed_travel_time_status", "errors"),
|
||||
[
|
||||
(400, {CONF_API_KEY: "invalid_api_key"}),
|
||||
(404, {"base": "cannot_connect"}),
|
||||
],
|
||||
)
|
||||
async def test_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_travel_time: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
failed_travel_time_status: int,
|
||||
errors: dict[str, str],
|
||||
) -> None:
|
||||
"""Test that the user step works."""
|
||||
mock_travel_time.get_all_travel_times.side_effect = WsdotTravelError(
|
||||
status=failed_travel_time_status
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=VALID_USER_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == errors
|
||||
|
||||
mock_travel_time.get_all_travel_times.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=VALID_USER_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_subentries",
|
||||
[
|
||||
[],
|
||||
],
|
||||
)
|
||||
async def test_create_travel_time_subentry(
|
||||
hass: HomeAssistant,
|
||||
mock_travel_time: AsyncMock,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the user step for Travel Time works."""
|
||||
# No user data; form is being show for the first time
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(init_integration.entry_id, SUBENTRY_TRAVEL_TIMES),
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# User data; the user made a choice and hit submit
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(init_integration.entry_id, SUBENTRY_TRAVEL_TIMES),
|
||||
context={"source": SOURCE_USER},
|
||||
data=VALID_USER_TRAVEL_TIME_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_NAME] == "Seattle-Bellevue via I-90 (EB AM)"
|
||||
assert result["data"][CONF_ID] == 96
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"import_config",
|
||||
[
|
||||
{
|
||||
CONF_API_KEY: "abcd-5678",
|
||||
CONF_TRAVEL_TIMES: [{CONF_ID: 96, CONF_NAME: "I-90 EB"}],
|
||||
},
|
||||
{
|
||||
CONF_API_KEY: "abcd-5678",
|
||||
CONF_TRAVEL_TIMES: [{CONF_ID: "96", CONF_NAME: "I-90 EB"}],
|
||||
},
|
||||
],
|
||||
ids=["with-int-id", "with-str-id"],
|
||||
)
|
||||
async def test_create_import_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_travel_time: AsyncMock,
|
||||
import_config: dict[str, str | int],
|
||||
) -> None:
|
||||
"""Test that the yaml import works."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=import_config,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "wsdot"
|
||||
assert result["data"][CONF_API_KEY] == "abcd-5678"
|
||||
|
||||
entry = result["result"]
|
||||
assert entry is not None
|
||||
assert len(entry.subentries) == 1
|
||||
subentry = next(iter(entry.subentries.values()))
|
||||
assert subentry.subentry_type == SUBENTRY_TRAVEL_TIMES
|
||||
assert subentry.title == "Seattle-Bellevue via I-90 (EB AM)"
|
||||
assert subentry.data[CONF_NAME] == "Seattle-Bellevue via I-90 (EB AM)"
|
||||
assert subentry.data[CONF_ID] == 96
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("failed_travel_time_status", "abort_reason"),
|
||||
[
|
||||
(400, "invalid_api_key"),
|
||||
(404, "cannot_connect"),
|
||||
],
|
||||
)
|
||||
async def test_failed_import_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_failed_travel_time: AsyncMock,
|
||||
mock_config_data: dict[str, Any],
|
||||
failed_travel_time_status: int,
|
||||
abort_reason: str,
|
||||
) -> None:
|
||||
"""Test the failure modes of a yaml import."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=mock_config_data,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == abort_reason
|
||||
|
||||
|
||||
async def test_incorrect_import_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_travel_time: AsyncMock,
|
||||
mock_config_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test a yaml import of a non-existent route."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "abcd-5678",
|
||||
CONF_TRAVEL_TIMES: [{CONF_ID: "100001", CONF_NAME: "nowhere"}],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "invalid_travel_time_id"
|
||||
|
||||
|
||||
async def test_import_integration_already_exists(
|
||||
hass: HomeAssistant,
|
||||
mock_travel_time: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we only allow one entry per API key."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "abcd-1234",
|
||||
CONF_TRAVEL_TIMES: [{CONF_ID: "100001", CONF_NAME: "nowhere"}],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_integration_already_exists(
|
||||
hass: HomeAssistant,
|
||||
mock_travel_time: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we only allow one entry per API key."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert not result["errors"]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=VALID_USER_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_travel_route_already_exists(
|
||||
hass: HomeAssistant,
|
||||
mock_travel_time: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we only allow choosing a travel time route once."""
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(init_integration.entry_id, SUBENTRY_TRAVEL_TIMES),
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert not result["errors"]
|
||||
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=VALID_USER_TRAVEL_TIME_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
21
tests/components/wsdot/test_init.py
Normal file
21
tests/components/wsdot/test_init.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""The tests for the WSDOT platform."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_travel_sensor_setup_no_auth(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_failed_travel_time: None,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the wsdot Travel Time sensor does not create an entry with a bad API key."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
@@ -1,41 +1,84 @@
|
||||
"""The tests for the WSDOT platform."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.components.wsdot.sensor import (
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.wsdot.const import CONF_TRAVEL_TIMES, DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_TRAVEL_TIMES,
|
||||
DOMAIN,
|
||||
CONF_PLATFORM,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
config = {
|
||||
CONF_API_KEY: "foo",
|
||||
CONF_TRAVEL_TIMES: [{CONF_ID: 96, CONF_NAME: "I90 EB"}],
|
||||
}
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_setup_with_config(
|
||||
hass: HomeAssistant, mock_travel_time: AsyncMock
|
||||
async def test_travel_sensor_details(
|
||||
hass: HomeAssistant,
|
||||
mock_travel_time: AsyncMock,
|
||||
init_integration: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the platform setup with configuration."""
|
||||
assert await async_setup_component(
|
||||
hass, "sensor", {"sensor": [{CONF_PLATFORM: DOMAIN, **config}]}
|
||||
)
|
||||
"""Test the wsdot Travel Time sensor details."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
state = hass.states.get("sensor.i90_eb")
|
||||
assert state is not None
|
||||
assert state.name == "I90 EB"
|
||||
assert state.state == "11"
|
||||
assert (
|
||||
state.attributes["Description"]
|
||||
== "Downtown Seattle to Downtown Bellevue via I-90"
|
||||
|
||||
async def test_travel_sensor_platform_setup(
|
||||
hass: HomeAssistant, mock_travel_time: AsyncMock, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the wsdot Travel Time sensor still supports setup from platform config."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
Platform.SENSOR,
|
||||
{
|
||||
Platform.SENSOR: [
|
||||
{
|
||||
CONF_PLATFORM: DOMAIN,
|
||||
CONF_API_KEY: "foo",
|
||||
CONF_TRAVEL_TIMES: [{CONF_ID: 96, CONF_NAME: "I90 EB"}],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
assert state.attributes["TimeUpdated"] == datetime(
|
||||
2017, 1, 21, 15, 10, tzinfo=timezone(timedelta(hours=-8))
|
||||
await hass.async_block_till_done()
|
||||
entry = next(iter(hass.config_entries.async_entries(DOMAIN)), None)
|
||||
assert entry is not None
|
||||
assert entry.data[CONF_API_KEY] == "foo"
|
||||
assert len(entry.subentries) == 1
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
|
||||
async def test_travel_sensor_platform_setup_bad_routes(
|
||||
hass: HomeAssistant, mock_travel_time: AsyncMock, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the wsdot Travel Time sensor platform upgrade skips unknown route ids."""
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
Platform.SENSOR,
|
||||
{
|
||||
Platform.SENSOR: [
|
||||
{
|
||||
CONF_PLATFORM: DOMAIN,
|
||||
CONF_API_KEY: "foo",
|
||||
CONF_TRAVEL_TIMES: [{CONF_ID: 4096, CONF_NAME: "Mars Expressway"}],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entry = next(iter(hass.config_entries.async_entries(DOMAIN)), None)
|
||||
assert entry is None
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue = issue_registry.async_get_issue(
|
||||
DOMAIN, "deprecated_yaml_import_issue_invalid_travel_time_id"
|
||||
)
|
||||
assert issue
|
||||
|
||||
Reference in New Issue
Block a user