Update greeclimate to 2.0.0 (#121030)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Clifford Roche 2024-08-05 05:18:34 -04:00 committed by GitHub
parent e9e357b12e
commit 1163cc7cab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 190 additions and 105 deletions

View File

@ -18,3 +18,5 @@ FAN_MEDIUM_HIGH = "medium high"
MAX_ERRORS = 2
TARGET_TEMPERATURE_STEP = 1
UPDATE_INTERVAL = 60

View File

@ -2,16 +2,20 @@
from __future__ import annotations
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from typing import Any
from greeclimate.device import Device, DeviceInfo
from greeclimate.discovery import Discovery, Listener
from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError
from greeclimate.network import Response
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.dt import utcnow
from .const import (
COORDINATORS,
@ -19,12 +23,13 @@ from .const import (
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
MAX_ERRORS,
UPDATE_INTERVAL,
)
_LOGGER = logging.getLogger(__name__)
class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Manages polling for state changes from the device."""
def __init__(self, hass: HomeAssistant, device: Device) -> None:
@ -34,28 +39,68 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
hass,
_LOGGER,
name=f"{DOMAIN}-{device.device_info.name}",
update_interval=timedelta(seconds=60),
update_interval=timedelta(seconds=UPDATE_INTERVAL),
always_update=False,
)
self.device = device
self._error_count = 0
self.device.add_handler(Response.DATA, self.device_state_updated)
self.device.add_handler(Response.RESULT, self.device_state_updated)
async def _async_update_data(self):
self._error_count: int = 0
self._last_response_time: datetime = utcnow()
self._last_error_time: datetime | None = None
def device_state_updated(self, *args: Any) -> None:
"""Handle device state updates."""
_LOGGER.debug("Device state updated: %s", json_dumps(args))
self._error_count = 0
self._last_response_time = utcnow()
self.async_set_updated_data(self.device.raw_properties)
async def _async_update_data(self) -> dict[str, Any]:
"""Update the state of the device."""
_LOGGER.debug(
"Updating device state: %s, error count: %d", self.name, self._error_count
)
try:
await self.device.update_state()
except DeviceNotBoundError as error:
raise UpdateFailed(f"Device {self.name} is unavailable") from error
raise UpdateFailed(
f"Device {self.name} is unavailable, device is not bound."
) from error
except DeviceTimeoutError as error:
self._error_count += 1
# Under normal conditions GREE units timeout every once in a while
if self.last_update_success and self._error_count >= MAX_ERRORS:
_LOGGER.warning(
"Device is unavailable: %s (%s)",
self.name,
self.device.device_info,
"Device %s is unavailable: %s", self.name, self.device.device_info
)
raise UpdateFailed(f"Device {self.name} is unavailable") from error
raise UpdateFailed(
f"Device {self.name} is unavailable, could not send update request"
) from error
else:
# raise update failed if time for more than MAX_ERRORS has passed since last update
now = utcnow()
elapsed_success = now - self._last_response_time
if self.update_interval and elapsed_success >= self.update_interval:
if not self._last_error_time or (
(now - self.update_interval) >= self._last_error_time
):
self._last_error_time = now
self._error_count += 1
_LOGGER.warning(
"Device %s is unresponsive for %s seconds",
self.name,
elapsed_success,
)
if self.last_update_success and self._error_count >= MAX_ERRORS:
raise UpdateFailed(
f"Device {self.name} is unresponsive for too long and now unavailable"
)
return self.device.raw_properties
async def push_state_update(self):
"""Send state updates to the physical device."""

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/gree",
"iot_class": "local_polling",
"loggers": ["greeclimate"],
"requirements": ["greeclimate==1.4.6"]
"requirements": ["greeclimate==2.0.0"]
}

View File

@ -1010,7 +1010,7 @@ gpiozero==1.6.2
gps3==0.33.3
# homeassistant.components.gree
greeclimate==1.4.6
greeclimate==2.0.0
# homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3

View File

@ -854,7 +854,7 @@ govee-local-api==1.5.1
gps3==0.33.3
# homeassistant.components.gree
greeclimate==1.4.6
greeclimate==2.0.0
# homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3

View File

@ -5,8 +5,12 @@ from datetime import timedelta
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.climate import DOMAIN
from homeassistant.components.gree.const import COORDINATORS, DOMAIN as GREE
from homeassistant.components.climate import DOMAIN, HVACMode
from homeassistant.components.gree.const import (
COORDINATORS,
DOMAIN as GREE,
UPDATE_INTERVAL,
)
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util
@ -69,3 +73,30 @@ async def test_discovery_after_setup(
device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]]
assert device_infos[0].ip == "1.1.1.2"
assert device_infos[1].ip == "2.2.2.1"
async def test_coordinator_updates(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Test gree devices update their state."""
await async_setup_gree(hass)
await hass.async_block_till_done()
assert len(hass.states.async_all(DOMAIN)) == 1
callback = device().add_handler.call_args_list[0][0][1]
async def fake_update_state(*args) -> None:
"""Fake update state."""
device().power = True
callback()
device().update_state.side_effect = fake_update_state
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID_1)
assert state is not None
assert state.state != HVACMode.OFF

View File

@ -48,7 +48,12 @@ from homeassistant.components.gree.climate import (
HVAC_MODES_REVERSE,
GreeClimateEntity,
)
from homeassistant.components.gree.const import FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW
from homeassistant.components.gree.const import (
DISCOVERY_SCAN_INTERVAL,
FAN_MEDIUM_HIGH,
FAN_MEDIUM_LOW,
UPDATE_INTERVAL,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
@ -61,7 +66,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util
from .common import async_setup_gree, build_device_mock
@ -70,12 +74,6 @@ from tests.common import async_fire_time_changed
ENTITY_ID = f"{DOMAIN}.fake_device_1"
@pytest.fixture
def mock_now():
"""Fixture for dtutil.now."""
return dt_util.utcnow()
async def test_discovery_called_once(hass: HomeAssistant, discovery, device) -> None:
"""Test discovery is only ever called once."""
await async_setup_gree(hass)
@ -104,7 +102,7 @@ async def test_discovery_setup(hass: HomeAssistant, discovery, device) -> None:
async def test_discovery_setup_connection_error(
hass: HomeAssistant, discovery, device, mock_now
hass: HomeAssistant, discovery, device
) -> None:
"""Test gree integration is setup."""
MockDevice1 = build_device_mock(
@ -126,7 +124,7 @@ async def test_discovery_setup_connection_error(
async def test_discovery_after_setup(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Test gree devices don't change after multiple discoveries."""
MockDevice1 = build_device_mock(
@ -142,8 +140,7 @@ async def test_discovery_after_setup(
discovery.return_value.mock_devices = [MockDevice1, MockDevice2]
device.side_effect = [MockDevice1, MockDevice2]
await async_setup_gree(hass)
await hass.async_block_till_done()
await async_setup_gree(hass) # Update 1
assert discovery.return_value.scan_count == 1
assert len(hass.states.async_all(DOMAIN)) == 2
@ -152,9 +149,8 @@ async def test_discovery_after_setup(
discovery.return_value.mock_devices = [MockDevice1, MockDevice2]
device.side_effect = [MockDevice1, MockDevice2]
next_update = mock_now + timedelta(minutes=6)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert discovery.return_value.scan_count == 2
@ -162,7 +158,7 @@ async def test_discovery_after_setup(
async def test_discovery_add_device_after_setup(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Test gree devices can be added after initial setup."""
MockDevice1 = build_device_mock(
@ -178,6 +174,8 @@ async def test_discovery_add_device_after_setup(
discovery.return_value.mock_devices = [MockDevice1]
device.side_effect = [MockDevice1]
await async_setup_gree(hass) # Update 1
await async_setup_gree(hass)
await hass.async_block_till_done()
@ -188,9 +186,8 @@ async def test_discovery_add_device_after_setup(
discovery.return_value.mock_devices = [MockDevice2]
device.side_effect = [MockDevice2]
next_update = mock_now + timedelta(minutes=6)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert discovery.return_value.scan_count == 2
@ -198,7 +195,7 @@ async def test_discovery_add_device_after_setup(
async def test_discovery_device_bind_after_setup(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Test gree devices can be added after a late device bind."""
MockDevice1 = build_device_mock(
@ -210,8 +207,7 @@ async def test_discovery_device_bind_after_setup(
discovery.return_value.mock_devices = [MockDevice1]
device.return_value = MockDevice1
await async_setup_gree(hass)
await hass.async_block_till_done()
await async_setup_gree(hass) # Update 1
assert len(hass.states.async_all(DOMAIN)) == 1
state = hass.states.get(ENTITY_ID)
@ -222,9 +218,8 @@ async def test_discovery_device_bind_after_setup(
MockDevice1.bind.side_effect = None
MockDevice1.update_state.side_effect = None
next_update = mock_now + timedelta(minutes=5)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
@ -232,7 +227,7 @@ async def test_discovery_device_bind_after_setup(
async def test_update_connection_failure(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, device, mock_now
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Testing update hvac connection failure exception."""
device().update_state.side_effect = [
@ -241,36 +236,32 @@ async def test_update_connection_failure(
DeviceTimeoutError,
]
await async_setup_gree(hass)
await async_setup_gree(hass) # Update 1
async def run_update():
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass)
next_update = mock_now + timedelta(minutes=5)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
# First update to make the device available
# Update 2
await run_update()
state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE
next_update = mock_now + timedelta(minutes=10)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
# Update 3
await run_update()
next_update = mock_now + timedelta(minutes=15)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
# Then two more update failures to make the device unavailable
# Update 4
await run_update()
state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1"
assert state.state == STATE_UNAVAILABLE
async def test_update_connection_failure_recovery(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now
async def test_update_connection_send_failure_recovery(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Testing update hvac connection failure recovery."""
device().update_state.side_effect = [
@ -279,31 +270,27 @@ async def test_update_connection_failure_recovery(
DEFAULT_MOCK,
]
await async_setup_gree(hass)
await async_setup_gree(hass) # Update 1
async def run_update():
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass)
# First update becomes unavailable
next_update = mock_now + timedelta(minutes=5)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
await run_update() # Update 2
state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1"
assert state.state == STATE_UNAVAILABLE
# Second update restores the connection
next_update = mock_now + timedelta(minutes=10)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
await run_update() # Update 3
state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE
async def test_update_unhandled_exception(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Testing update hvac connection unhandled response exception."""
device().update_state.side_effect = [DEFAULT_MOCK, Exception]
@ -314,9 +301,8 @@ async def test_update_unhandled_exception(
assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE
next_update = mock_now + timedelta(minutes=10)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
@ -325,15 +311,13 @@ async def test_update_unhandled_exception(
async def test_send_command_device_timeout(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Test for sending power on command to the device with a device timeout."""
await async_setup_gree(hass)
# First update to make the device available
next_update = mock_now + timedelta(minutes=5)
freezer.move_to(next_update)
async_fire_time_changed(hass, next_update)
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
@ -355,7 +339,40 @@ async def test_send_command_device_timeout(
assert state.state != STATE_UNAVAILABLE
async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) -> None:
async def test_unresponsive_device(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device
) -> None:
"""Test for unresponsive device."""
await async_setup_gree(hass)
async def run_update():
freezer.tick(timedelta(seconds=UPDATE_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Update 2
await run_update()
state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE
# Update 3, 4, 5
await run_update()
await run_update()
await run_update()
state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1"
assert state.state == STATE_UNAVAILABLE
# Receiving update from device will reset the state to available again
device().device_state_updated("test")
await run_update()
state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE
async def test_send_power_on(hass: HomeAssistant, discovery, device) -> None:
"""Test for sending power on command to the device."""
await async_setup_gree(hass)
@ -372,7 +389,7 @@ async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) -
async def test_send_power_off_device_timeout(
hass: HomeAssistant, discovery, device, mock_now
hass: HomeAssistant, discovery, device
) -> None:
"""Test for sending power off command to the device with a device timeout."""
device().push_state_update.side_effect = DeviceTimeoutError
@ -543,9 +560,7 @@ async def test_update_target_temperature(
@pytest.mark.parametrize(
"preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE]
)
async def test_send_preset_mode(
hass: HomeAssistant, discovery, device, mock_now, preset
) -> None:
async def test_send_preset_mode(hass: HomeAssistant, discovery, device, preset) -> None:
"""Test for sending preset mode command to the device."""
await async_setup_gree(hass)
@ -561,9 +576,7 @@ async def test_send_preset_mode(
assert state.attributes.get(ATTR_PRESET_MODE) == preset
async def test_send_invalid_preset_mode(
hass: HomeAssistant, discovery, device, mock_now
) -> None:
async def test_send_invalid_preset_mode(hass: HomeAssistant, discovery, device) -> None:
"""Test for sending preset mode command to the device."""
await async_setup_gree(hass)
@ -584,7 +597,7 @@ async def test_send_invalid_preset_mode(
"preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE]
)
async def test_send_preset_mode_device_timeout(
hass: HomeAssistant, discovery, device, mock_now, preset
hass: HomeAssistant, discovery, device, preset
) -> None:
"""Test for sending preset mode command to the device with a device timeout."""
device().push_state_update.side_effect = DeviceTimeoutError
@ -607,7 +620,7 @@ async def test_send_preset_mode_device_timeout(
"preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE]
)
async def test_update_preset_mode(
hass: HomeAssistant, discovery, device, mock_now, preset
hass: HomeAssistant, discovery, device, preset
) -> None:
"""Test for updating preset mode from the device."""
device().steady_heat = preset == PRESET_AWAY
@ -634,7 +647,7 @@ async def test_update_preset_mode(
],
)
async def test_send_hvac_mode(
hass: HomeAssistant, discovery, device, mock_now, hvac_mode
hass: HomeAssistant, discovery, device, hvac_mode
) -> None:
"""Test for sending hvac mode command to the device."""
await async_setup_gree(hass)
@ -656,7 +669,7 @@ async def test_send_hvac_mode(
[HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT],
)
async def test_send_hvac_mode_device_timeout(
hass: HomeAssistant, discovery, device, mock_now, hvac_mode
hass: HomeAssistant, discovery, device, hvac_mode
) -> None:
"""Test for sending hvac mode command to the device with a device timeout."""
device().push_state_update.side_effect = DeviceTimeoutError
@ -687,7 +700,7 @@ async def test_send_hvac_mode_device_timeout(
],
)
async def test_update_hvac_mode(
hass: HomeAssistant, discovery, device, mock_now, hvac_mode
hass: HomeAssistant, discovery, device, hvac_mode
) -> None:
"""Test for updating hvac mode from the device."""
device().power = hvac_mode != HVACMode.OFF
@ -704,9 +717,7 @@ async def test_update_hvac_mode(
"fan_mode",
[FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH],
)
async def test_send_fan_mode(
hass: HomeAssistant, discovery, device, mock_now, fan_mode
) -> None:
async def test_send_fan_mode(hass: HomeAssistant, discovery, device, fan_mode) -> None:
"""Test for sending fan mode command to the device."""
await async_setup_gree(hass)
@ -722,9 +733,7 @@ async def test_send_fan_mode(
assert state.attributes.get(ATTR_FAN_MODE) == fan_mode
async def test_send_invalid_fan_mode(
hass: HomeAssistant, discovery, device, mock_now
) -> None:
async def test_send_invalid_fan_mode(hass: HomeAssistant, discovery, device) -> None:
"""Test for sending fan mode command to the device."""
await async_setup_gree(hass)
@ -746,7 +755,7 @@ async def test_send_invalid_fan_mode(
[FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH],
)
async def test_send_fan_mode_device_timeout(
hass: HomeAssistant, discovery, device, mock_now, fan_mode
hass: HomeAssistant, discovery, device, fan_mode
) -> None:
"""Test for sending fan mode command to the device with a device timeout."""
device().push_state_update.side_effect = DeviceTimeoutError
@ -770,7 +779,7 @@ async def test_send_fan_mode_device_timeout(
[FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH],
)
async def test_update_fan_mode(
hass: HomeAssistant, discovery, device, mock_now, fan_mode
hass: HomeAssistant, discovery, device, fan_mode
) -> None:
"""Test for updating fan mode from the device."""
device().fan_speed = FAN_MODES_REVERSE.get(fan_mode)
@ -786,7 +795,7 @@ async def test_update_fan_mode(
"swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL]
)
async def test_send_swing_mode(
hass: HomeAssistant, discovery, device, mock_now, swing_mode
hass: HomeAssistant, discovery, device, swing_mode
) -> None:
"""Test for sending swing mode command to the device."""
await async_setup_gree(hass)
@ -803,9 +812,7 @@ async def test_send_swing_mode(
assert state.attributes.get(ATTR_SWING_MODE) == swing_mode
async def test_send_invalid_swing_mode(
hass: HomeAssistant, discovery, device, mock_now
) -> None:
async def test_send_invalid_swing_mode(hass: HomeAssistant, discovery, device) -> None:
"""Test for sending swing mode command to the device."""
await async_setup_gree(hass)
@ -826,7 +833,7 @@ async def test_send_invalid_swing_mode(
"swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL]
)
async def test_send_swing_mode_device_timeout(
hass: HomeAssistant, discovery, device, mock_now, swing_mode
hass: HomeAssistant, discovery, device, swing_mode
) -> None:
"""Test for sending swing mode command to the device with a device timeout."""
device().push_state_update.side_effect = DeviceTimeoutError
@ -849,7 +856,7 @@ async def test_send_swing_mode_device_timeout(
"swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL]
)
async def test_update_swing_mode(
hass: HomeAssistant, discovery, device, mock_now, swing_mode
hass: HomeAssistant, discovery, device, swing_mode
) -> None:
"""Test for updating swing mode from the device."""
device().horizontal_swing = (