mirror of
https://github.com/home-assistant/core.git
synced 2026-05-29 12:14:26 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f80c71d524 | |||
| 5681ba40f1 | |||
| 8a9a1c5fed | |||
| c587e101af | |||
| 6eeeac46f3 | |||
| 86542b8ad0 | |||
| 7e07e7062c | |||
| d7c13fee27 | |||
| a0a44f7a25 |
+7
-7
@@ -36,7 +36,7 @@
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
# - github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
# - github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
|
||||
#
|
||||
# Container images used:
|
||||
# - ghcr.io/github/gh-aw-firewall/agent:0.25.46
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -352,7 +352,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -961,7 +961,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1100,7 +1100,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1325,7 +1325,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1383,7 +1383,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@318d7f4901f78b85e25b91709cf0109ac9b425f6 # v0.74.9
|
||||
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
|
||||
+1
@@ -57,6 +57,7 @@ OVERKIZ_TO_HVAC_MODE: dict[str, HVACMode] = {
|
||||
OverkizCommandParam.STANDBY: HVACMode.OFF, # main command
|
||||
OverkizCommandParam.AUTO: HVACMode.AUTO,
|
||||
OverkizCommandParam.EXTERNAL: HVACMode.AUTO,
|
||||
OverkizCommandParam.PROG: HVACMode.AUTO,
|
||||
OverkizCommandParam.INTERNAL: HVACMode.AUTO, # main command
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ class IRobotEntity(Entity):
|
||||
model=self.vacuum_state.get("sku"),
|
||||
name=str(self.vacuum_state.get("name")),
|
||||
sw_version=self.vacuum_state.get("softwareVer"),
|
||||
hw_version=self.vacuum_state.get("hardwareRev"),
|
||||
hw_version=(
|
||||
str(hw_rev)
|
||||
if (hw_rev := self.vacuum_state.get("hardwareRev")) is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
if mac_address := self.vacuum_state.get("hwPartsRev", {}).get(
|
||||
|
||||
@@ -40,6 +40,8 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
login_task: asyncio.Task | None = None
|
||||
refresh_token: str | None = None
|
||||
tado: Tado | None = None
|
||||
tado_device_url: str = ""
|
||||
user_code: str = ""
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
@@ -69,8 +71,8 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Error while initiating Tado")
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
assert self.tado is not None
|
||||
tado_device_url = self.tado.device_verification_url()
|
||||
user_code = URL(tado_device_url).query["user_code"]
|
||||
self.tado_device_url = self.tado.device_verification_url()
|
||||
self.user_code = URL(self.tado_device_url).query["user_code"]
|
||||
|
||||
async def _wait_for_login() -> None:
|
||||
"""Wait for the user to login."""
|
||||
@@ -119,8 +121,8 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user",
|
||||
progress_action="wait_for_device",
|
||||
description_placeholders={
|
||||
"url": tado_device_url,
|
||||
"code": user_code,
|
||||
"url": self.tado_device_url,
|
||||
"code": self.user_code,
|
||||
},
|
||||
progress_task=self.login_task,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from homeassistant.components.device_tracker import TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -83,11 +82,3 @@ class TeslaFleetDeviceTrackerRouteEntity(TeslaFleetDeviceTrackerEntity):
|
||||
self.get("drive_state_active_route_longitude", False) is None
|
||||
or self.get("drive_state_active_route_latitude", False) is None
|
||||
)
|
||||
|
||||
@property
|
||||
def location_name(self) -> str | None:
|
||||
"""Return a location name for the current location of the device."""
|
||||
location = self.get("drive_state_active_route_destination")
|
||||
if location == "Home":
|
||||
return STATE_HOME
|
||||
return location
|
||||
|
||||
@@ -280,6 +280,10 @@ VEHICLE_DESCRIPTIONS: tuple[TeslaFleetSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
),
|
||||
TeslaFleetSensorEntityDescription(
|
||||
key="drive_state_active_route_destination",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.device_tracker import (
|
||||
TrackerEntity,
|
||||
TrackerEntityDescription,
|
||||
)
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -31,12 +30,6 @@ class TeslemetryDeviceTrackerEntityDescription(TrackerEntityDescription):
|
||||
[TeslemetryStreamVehicle, Callable[[TeslaLocation | None], None]],
|
||||
Callable[[], None],
|
||||
]
|
||||
name_listener: (
|
||||
Callable[
|
||||
[TeslemetryStreamVehicle, Callable[[str | None], None]], Callable[[], None]
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
streaming_firmware: str
|
||||
polling_prefix: str | None = None
|
||||
|
||||
@@ -54,9 +47,6 @@ DESCRIPTIONS: tuple[TeslemetryDeviceTrackerEntityDescription, ...] = (
|
||||
value_listener=lambda vehicle, callback: vehicle.listen_DestinationLocation(
|
||||
callback
|
||||
),
|
||||
name_listener=lambda vehicle, callback: vehicle.listen_DestinationName(
|
||||
callback
|
||||
),
|
||||
streaming_firmware="2024.26",
|
||||
),
|
||||
TeslemetryDeviceTrackerEntityDescription(
|
||||
@@ -126,11 +116,6 @@ class TeslemetryVehiclePollingDeviceTrackerEntity(
|
||||
self._attr_longitude = self.get(
|
||||
f"{self.entity_description.polling_prefix}_longitude"
|
||||
)
|
||||
self._attr_location_name = self.get(
|
||||
f"{self.entity_description.polling_prefix}_destination"
|
||||
)
|
||||
if self._attr_location_name == "Home":
|
||||
self._attr_location_name = STATE_HOME
|
||||
self._attr_available = (
|
||||
self._attr_latitude is not None and self._attr_longitude is not None
|
||||
)
|
||||
@@ -158,28 +143,14 @@ class TeslemetryStreamingDeviceTrackerEntity(
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
self._attr_latitude = state.attributes.get("latitude")
|
||||
self._attr_longitude = state.attributes.get("longitude")
|
||||
self._attr_location_name = state.attributes.get("location_name")
|
||||
self.async_on_remove(
|
||||
self.entity_description.value_listener(
|
||||
self.vehicle.stream_vehicle, self._location_callback
|
||||
)
|
||||
)
|
||||
if self.entity_description.name_listener:
|
||||
self.async_on_remove(
|
||||
self.entity_description.name_listener(
|
||||
self.vehicle.stream_vehicle, self._name_callback
|
||||
)
|
||||
)
|
||||
|
||||
def _location_callback(self, location: TeslaLocation | None) -> None:
|
||||
"""Update the value of the entity."""
|
||||
self._attr_latitude = None if location is None else location.latitude
|
||||
self._attr_longitude = None if location is None else location.longitude
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _name_callback(self, name: str | None) -> None:
|
||||
"""Update the value of the entity."""
|
||||
self._attr_location_name = name
|
||||
if self._attr_location_name == "Home":
|
||||
self._attr_location_name = STATE_HOME
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -510,6 +510,14 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_active_route_destination",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_DestinationName(
|
||||
callback
|
||||
),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_active_route_traffic_minutes_delay",
|
||||
polling=True,
|
||||
|
||||
@@ -468,7 +468,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
|
||||
async def on_restart(self) -> None:
|
||||
"""Block until pipeline loop will be restarted."""
|
||||
_LOGGER.warning(
|
||||
_LOGGER.debug(
|
||||
"Satellite has been disconnected. Reconnecting in %s second(s)",
|
||||
_RECONNECT_SECONDS,
|
||||
)
|
||||
|
||||
@@ -138,6 +138,8 @@ SAVE_DELAY = 1
|
||||
|
||||
DISCOVERY_COOLDOWN = 1
|
||||
|
||||
SETUP_RETRY_MAX_WAIT = 600 # 10 minutes
|
||||
|
||||
ISSUE_UNIQUE_ID_COLLISION = "config_entry_unique_id_collision"
|
||||
UNIQUE_ID_COLLISION_TITLE_LIMIT = 5
|
||||
|
||||
@@ -836,7 +838,7 @@ class ConfigEntry[_DataT = Any]:
|
||||
error_reason_translation_key,
|
||||
error_reason_translation_placeholders,
|
||||
)
|
||||
wait_time = 2 ** min(self._tries, 4) * 5 + (
|
||||
wait_time = min(2**self._tries * 5, SETUP_RETRY_MAX_WAIT) + (
|
||||
randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) / 1000000
|
||||
)
|
||||
self._tries += 1
|
||||
|
||||
@@ -1 +1,33 @@
|
||||
"""Tests for the Duco integration."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the full Duco integration for testing."""
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return config_entry
|
||||
|
||||
|
||||
async def setup_platform_integration(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
platforms: Sequence[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up selected Duco platforms for testing."""
|
||||
config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.duco.PLATFORMS", list(platforms)):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return config_entry
|
||||
|
||||
@@ -23,6 +23,8 @@ from homeassistant.components.duco.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_array_fixture
|
||||
|
||||
TEST_HOST = "192.168.1.100"
|
||||
@@ -159,112 +161,7 @@ def mock_lan_info() -> LanInfo:
|
||||
@pytest.fixture
|
||||
def mock_nodes() -> list[Node]:
|
||||
"""Return a list of nodes covering all supported types."""
|
||||
return [
|
||||
Node(
|
||||
node_id=1,
|
||||
general=NodeGeneralInfo(
|
||||
node_type="BOX",
|
||||
sub_type=1,
|
||||
network_type="VIRT",
|
||||
parent=0,
|
||||
asso=0,
|
||||
name="Living",
|
||||
identify=0,
|
||||
),
|
||||
ventilation=NodeVentilationInfo(
|
||||
state="AUTO",
|
||||
time_state_remain=0,
|
||||
time_state_end=0,
|
||||
mode="AUTO",
|
||||
flow_lvl_tgt=0,
|
||||
),
|
||||
sensor=NodeSensorInfo(
|
||||
co2=None,
|
||||
iaq_co2=None,
|
||||
rh=None,
|
||||
iaq_rh=None,
|
||||
temp=27.9,
|
||||
),
|
||||
),
|
||||
Node(
|
||||
node_id=2,
|
||||
general=NodeGeneralInfo(
|
||||
node_type="UCCO2",
|
||||
sub_type=0,
|
||||
network_type="RF",
|
||||
parent=1,
|
||||
asso=1,
|
||||
name="Office CO2",
|
||||
identify=0,
|
||||
),
|
||||
ventilation=NodeVentilationInfo(
|
||||
state="AUTO",
|
||||
time_state_remain=0,
|
||||
time_state_end=0,
|
||||
mode="-",
|
||||
flow_lvl_tgt=None,
|
||||
),
|
||||
sensor=NodeSensorInfo(
|
||||
co2=405,
|
||||
iaq_co2=80,
|
||||
rh=None,
|
||||
iaq_rh=None,
|
||||
temp=19.8,
|
||||
),
|
||||
),
|
||||
Node(
|
||||
node_id=113,
|
||||
general=NodeGeneralInfo(
|
||||
node_type="BSRH",
|
||||
sub_type=0,
|
||||
network_type="RF",
|
||||
parent=1,
|
||||
asso=1,
|
||||
name="Bathroom RH",
|
||||
identify=0,
|
||||
),
|
||||
ventilation=NodeVentilationInfo(
|
||||
state="AUTO",
|
||||
time_state_remain=0,
|
||||
time_state_end=0,
|
||||
mode="-",
|
||||
flow_lvl_tgt=None,
|
||||
),
|
||||
sensor=NodeSensorInfo(
|
||||
co2=None,
|
||||
iaq_co2=None,
|
||||
rh=42.0,
|
||||
iaq_rh=85,
|
||||
temp=27.9,
|
||||
),
|
||||
),
|
||||
Node(
|
||||
node_id=50,
|
||||
general=NodeGeneralInfo(
|
||||
node_type="UCRH",
|
||||
sub_type=0,
|
||||
network_type="RF",
|
||||
parent=1,
|
||||
asso=1,
|
||||
name="Kitchen RH",
|
||||
identify=0,
|
||||
),
|
||||
ventilation=NodeVentilationInfo(
|
||||
state="AUTO",
|
||||
time_state_remain=0,
|
||||
time_state_end=0,
|
||||
mode="-",
|
||||
flow_lvl_tgt=None,
|
||||
),
|
||||
sensor=NodeSensorInfo(
|
||||
co2=None,
|
||||
iaq_co2=None,
|
||||
rh=61.0,
|
||||
iaq_rh=90,
|
||||
temp=22.5,
|
||||
),
|
||||
),
|
||||
]
|
||||
return load_nodes_fixture("nodes.json")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -327,7 +224,4 @@ async def init_integration(
|
||||
mock_duco_client: AsyncMock,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Duco integration 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
|
||||
return await setup_integration(hass, mock_config_entry)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
[
|
||||
{
|
||||
"node_id": 1,
|
||||
"general": {
|
||||
"node_type": "BOX",
|
||||
"sub_type": 1,
|
||||
"network_type": "VIRT",
|
||||
"parent": 0,
|
||||
"asso": 0,
|
||||
"name": "Living",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "AUTO",
|
||||
"flow_lvl_tgt": 0
|
||||
},
|
||||
"sensor": {
|
||||
"co2": null,
|
||||
"iaq_co2": null,
|
||||
"rh": null,
|
||||
"iaq_rh": null,
|
||||
"temp": 27.9
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": 2,
|
||||
"general": {
|
||||
"node_type": "UCCO2",
|
||||
"sub_type": 0,
|
||||
"network_type": "RF",
|
||||
"parent": 1,
|
||||
"asso": 1,
|
||||
"name": "Office CO2",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "-",
|
||||
"flow_lvl_tgt": null
|
||||
},
|
||||
"sensor": {
|
||||
"co2": 405,
|
||||
"iaq_co2": 80,
|
||||
"rh": null,
|
||||
"iaq_rh": null,
|
||||
"temp": 19.8
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": 113,
|
||||
"general": {
|
||||
"node_type": "BSRH",
|
||||
"sub_type": 0,
|
||||
"network_type": "RF",
|
||||
"parent": 1,
|
||||
"asso": 1,
|
||||
"name": "Bathroom RH",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "-",
|
||||
"flow_lvl_tgt": null
|
||||
},
|
||||
"sensor": {
|
||||
"co2": null,
|
||||
"iaq_co2": null,
|
||||
"rh": 42.0,
|
||||
"iaq_rh": 85,
|
||||
"temp": 27.9
|
||||
}
|
||||
},
|
||||
{
|
||||
"node_id": 50,
|
||||
"general": {
|
||||
"node_type": "UCRH",
|
||||
"sub_type": 0,
|
||||
"network_type": "RF",
|
||||
"parent": 1,
|
||||
"asso": 1,
|
||||
"name": "Kitchen RH",
|
||||
"identify": 0
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "AUTO",
|
||||
"time_state_remain": 0,
|
||||
"time_state_end": 0,
|
||||
"mode": "-",
|
||||
"flow_lvl_tgt": null
|
||||
},
|
||||
"sensor": {
|
||||
"co2": null,
|
||||
"iaq_co2": null,
|
||||
"rh": 61.0,
|
||||
"iaq_rh": 90,
|
||||
"temp": 22.5
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for the Duco fan platform."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from duco_connectivity import DucoConnectionError, DucoError, DucoRateLimitError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
@@ -21,6 +21,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_platform_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
_FAN_ENTITY = "fan.living"
|
||||
@@ -33,11 +35,7 @@ async def init_integration(
|
||||
mock_duco_client: AsyncMock,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up only the fan platform for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.duco.PLATFORMS", [Platform.FAN]):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return mock_config_entry
|
||||
return await setup_platform_integration(hass, mock_config_entry, [Platform.FAN])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for the Duco sensor platform."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from duco_connectivity import (
|
||||
DucoConnectionError,
|
||||
@@ -22,6 +22,8 @@ from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import setup_platform_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@@ -34,11 +36,7 @@ async def init_integration(
|
||||
) -> MockConfigEntry:
|
||||
"""Set up only the sensor platform for testing."""
|
||||
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.duco.PLATFORMS", [Platform.SENSOR]):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return mock_config_entry
|
||||
return await setup_platform_integration(hass, mock_config_entry, [Platform.SENSOR])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
|
||||
@@ -231,6 +231,43 @@ async def test_options_flow(
|
||||
assert result["data"] == {CONF_FALLBACK: CONST_OVERLAY_TADO_DEFAULT}
|
||||
|
||||
|
||||
async def test_show_progress_polling(
|
||||
hass: HomeAssistant,
|
||||
mock_tado_api: MagicMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test progress step re-entry while login task is still running."""
|
||||
|
||||
event = threading.Event()
|
||||
|
||||
def mock_tado_api_device_activation() -> None:
|
||||
event.wait(timeout=5)
|
||||
|
||||
mock_tado_api.device_activation = mock_tado_api_device_activation
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "user"
|
||||
assert result["description_placeholders"]["url"] is not None
|
||||
assert result["description_placeholders"]["code"] == "TEST"
|
||||
|
||||
# Poll again while task is still running — this re-enters async_step_user
|
||||
# with self.tado already set
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["description_placeholders"]["url"] is not None
|
||||
assert result["description_placeholders"]["code"] == "TEST"
|
||||
|
||||
# Now complete the login
|
||||
event.set()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_homekit(hass: HomeAssistant, mock_tado_api: MagicMock) -> None:
|
||||
"""Test that we abort from homekit if tado is already setup."""
|
||||
|
||||
|
||||
@@ -108,6 +108,6 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'home',
|
||||
'state': 'not_home',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -3216,6 +3216,69 @@
|
||||
'state': 'stopped',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'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.test_destination',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Destination',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Destination',
|
||||
'platform': 'tesla_fleet',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'drive_state_active_route_destination',
|
||||
'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_destination',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Destination',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Home',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-statealt]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Destination',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_distance_to_arrival-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'home',
|
||||
'state': 'not_home',
|
||||
})
|
||||
# ---
|
||||
# name: test_device_tracker_alt[device_tracker.test_location-statealt]
|
||||
|
||||
@@ -3228,6 +3228,69 @@
|
||||
'state': 'stopped',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'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.test_destination',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Destination',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Destination',
|
||||
'platform': 'teslemetry',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'drive_state_active_route_destination',
|
||||
'unique_id': 'LRW3F7EK4NC700000-drive_state_active_route_destination',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Destination',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Home',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-statealt]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Destination',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_distance_to_arrival-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -81,7 +81,6 @@ async def test_device_tracker_streaming(
|
||||
"latitude": 3.0,
|
||||
"longitude": 4.0,
|
||||
},
|
||||
Signal.DESTINATION_NAME: "Home",
|
||||
Signal.ORIGIN_LOCATION: None,
|
||||
},
|
||||
"createdAt": "2024-10-04T10:45:17.537Z",
|
||||
@@ -91,7 +90,7 @@ async def test_device_tracker_streaming(
|
||||
|
||||
# Assert the entities have correct state values
|
||||
assert hass.states.get("device_tracker.test_location").state == "not_home"
|
||||
assert hass.states.get("device_tracker.test_route").state == "home"
|
||||
assert hass.states.get("device_tracker.test_route").state == "not_home"
|
||||
assert hass.states.get("device_tracker.test_origin").state == "unknown"
|
||||
|
||||
# Reload the entry
|
||||
|
||||
@@ -1702,6 +1702,44 @@ async def test_setup_raise_not_ready(
|
||||
assert entry.reason is None
|
||||
|
||||
|
||||
async def test_setup_not_ready_exponential_backoff(
|
||||
hass: HomeAssistant,
|
||||
manager: config_entries.ConfigEntries,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test setup retry uses exponential backoff capped at 10 minutes."""
|
||||
entry = MockConfigEntry(domain="test")
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
attempts = 0
|
||||
|
||||
async def _mock_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
mock_integration(hass, MockModule("test", async_setup_entry=_mock_setup_entry))
|
||||
mock_platform(hass, "test.config_flow", None)
|
||||
|
||||
await manager.async_setup(entry.entry_id)
|
||||
assert attempts == 1
|
||||
|
||||
expected_waits = [5, 10, 20, 40, 80, 160, 320, 600, 600]
|
||||
for i, wait in enumerate(expected_waits):
|
||||
# Advance to just before the retry should fire
|
||||
freezer.tick(wait - 1)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert attempts == i + 1, f"Retry {i + 1} fired too early"
|
||||
|
||||
# Advance past the retry point (+ 1s for jitter)
|
||||
freezer.tick(2)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert attempts == i + 2, f"Retry {i + 1} did not fire"
|
||||
assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_raise_not_ready_from_exception(
|
||||
hass: HomeAssistant,
|
||||
manager: config_entries.ConfigEntries,
|
||||
|
||||
Reference in New Issue
Block a user