Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion greeclimate/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ def __init__(self, device_info: DeviceInfo, timeout: int = 120, bind_timeout: in
self.device_info: DeviceInfo = device_info

self._bind_timeout = bind_timeout
self._update_state_complete = asyncio.Event()
Copy link
Owner

@cmroche cmroche Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An event signal at this level will be shared across multiple calls to update_state and not behave correctly.


""" Device properties """
self.hid = None
Expand Down Expand Up @@ -262,11 +263,14 @@ async def request_version(self) -> None:
except asyncio.TimeoutError:
raise DeviceTimeoutError

async def update_state(self, wait_for: float = 30):
async def update_state(self, wait_for: float = 30) -> None:
"""Update the internal state of the device structure of the physical device, 0 for no wait

Args:
wait_for (object): How long to wait for an update from the device

Raises:
DeviceTimeoutError: The device didn't respond within the timeout
"""
if not self.device_cipher:
await self.bind()
Expand All @@ -278,8 +282,12 @@ async def update_state(self, wait_for: float = 30):
props.append("hid")

try:
self._update_state_complete.clear()
await self.send(self.create_status_message(self.device_info, *props))

if wait_for > 0:
task = asyncio.create_task(self._update_state_complete.wait())
await asyncio.wait_for(task, timeout=wait_for)
except asyncio.TimeoutError:
raise DeviceTimeoutError

Expand All @@ -304,6 +312,8 @@ def handle_state_update(self, **kwargs) -> None:
self._logger.info(f"Device version changed to {self.version}, hid {self.hid}")
self._logger.debug(f"Using device temperature {self.current_temperature}")

self._update_state_complete.set()

async def push_state_update(self, wait_for: float = 30):
"""Push any pending state updates to the unit

Expand Down Expand Up @@ -332,8 +342,12 @@ async def push_state_update(self, wait_for: float = 30):
self._dirty.clear()

try:
self._update_state_complete.clear()
await self.send(self.create_command_message(self.device_info, **props))

if wait_for > 0:
task = asyncio.create_task(self._update_state_complete.wait())
await asyncio.wait_for(task, timeout=wait_for)
except asyncio.TimeoutError:
raise DeviceTimeoutError

Expand Down
78 changes: 57 additions & 21 deletions tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def fake_send(*args, **kwargs):
async def test_device_bind_timeout(cipher, send):
"""Check that the device handles timeout errors when binding."""
info = DeviceInfo(*get_mock_info())
device = Device(info, timeout=1)
device = Device(info, bind_timeout=1)

with pytest.raises(DeviceTimeoutError):
await device.bind()
Expand Down Expand Up @@ -271,14 +271,14 @@ def fake_send(*args, **kwargs):
device.ready.set()
send.side_effect = fake_send

await device.update_state()
await device.update_state(0)
assert send.call_count == 2
assert device.device_cipher.key == fake_key

device.power = True

send.side_effect = None
await device.push_state_update()
await device.push_state_update(0)

assert device.device_cipher is not None
assert device.device_cipher.key == fake_key
Expand Down Expand Up @@ -344,14 +344,23 @@ def fake_send(*args, **kwargs):

@pytest.mark.asyncio
async def test_update_properties_timeout(cipher, send):
"""Check that timeouts are handled when properties are updates."""
"""Check that timeouts are handled when properties are updated."""
device = await generate_device_mock_async()

send.side_effect = asyncio.TimeoutError
with pytest.raises(DeviceTimeoutError):
await device.update_state()


@pytest.mark.asyncio
async def test_update_properties_timeout_reply(cipher, send):
"""Check that reply timeouts are handled when properties are updated."""
device = await generate_device_mock_async()

with pytest.raises(DeviceTimeoutError):
await device.update_state(wait_for=1)


@pytest.mark.asyncio
async def test_set_properties_not_dirty(cipher, send):
"""Check that the state isn't pushed when properties unchanged."""
Expand Down Expand Up @@ -383,6 +392,11 @@ async def test_set_properties(cipher, send):
device.power_save = True
device.target_humidity = 30

def fake_send(*args, **kwargs):
state = get_mock_state_on()
device.handle_state_update(**state)
send.side_effect = fake_send

await device.push_state_update()
send.assert_called_once()

Expand Down Expand Up @@ -430,6 +444,34 @@ async def test_set_properties_timeout(cipher, send):
await device.push_state_update()


@pytest.mark.asyncio
async def test_set_properties_timeout_reply(cipher, send):
"""Check timeout handling when pushing state changes."""
device = await generate_device_mock_async()

device.power = True
device.mode = 1
device.temperature_units = 1
device.fan_speed = 1
device.fresh_air = True
device.xfan = True
device.anion = True
device.sleep = True
device.light = True
device.horizontal_swing = 1
device.vertical_swing = 1
device.quiet = True
device.turbo = True
device.steady_heat = True
device.power_save = True

assert len(device._dirty)

send.reset_mock()
with pytest.raises(DeviceTimeoutError):
await device.push_state_update(1)


@pytest.mark.asyncio
async def test_uninitialized_properties(cipher, send):
"""Check uninitialized property handling."""
Expand Down Expand Up @@ -569,14 +611,12 @@ async def test_send_temperature_celsius(temperature, cipher, send):
device.temperature_units = TemperatureUnits.C
device.target_temperature = temperature

await device.push_state_update()
assert send.call_count == 1

def fake_send(*args, **kwargs):
device.handle_state_update(**state)
send.side_effect = fake_send

await device.update_state()
await device.push_state_update()
assert send.call_count == 1

assert device.current_temperature == temperature

Expand All @@ -599,14 +639,12 @@ async def test_send_temperature_farenheit(temperature, cipher, send):
device.temperature_units = TemperatureUnits.F
device.target_temperature = temperature

await device.push_state_update()
assert send.call_count == 1

def fake_send(*args, **kwargs):
device.handle_state_update(**state)
send.side_effect = fake_send

await device.update_state()
await device.push_state_update()
assert send.call_count == 1

assert device.current_temperature == temperature

Expand Down Expand Up @@ -653,18 +691,18 @@ async def test_send_temperature_out_of_range_farenheit_get(temperature, cipher,

@pytest.mark.asyncio
async def test_enable_disable_sleep_mode(cipher, send):
"""Check that properties can be updates."""
"""Check that properties can be updated."""
device = await generate_device_mock_async()

device.sleep = True
await device.push_state_update()
await device.push_state_update(0)
assert send.call_count == 1

assert device.get_property(Props.SLEEP) == 1
assert device.get_property(Props.SLEEP_MODE) == 1

device.sleep = False
await device.push_state_update()
await device.push_state_update(0)
assert send.call_count == 2

assert device.get_property(Props.SLEEP) == 0
Expand All @@ -689,14 +727,12 @@ async def test_mismatch_temrec_farenheit(temperature, cipher, send):
device.temperature_units = TemperatureUnits.F
device.target_temperature = temperature

await device.push_state_update()
assert send.call_count == 1

def fake_send(*args, **kwargs):
device.handle_state_update(**state)
send.side_effect = None
send.side_effect = fake_send
await device.push_state_update()

await device.update_state()
assert send.call_count == 1

assert device.current_temperature == temperature

Expand Down Expand Up @@ -733,4 +769,4 @@ def test_device_key_set_get():
device.device_cipher = CipherV1()
device.device_key = "fake_key"
assert device.device_key == "fake_key"


4 changes: 2 additions & 2 deletions tests/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async def test_issue_69_TemSen_40_should_not_set_firmware_v4():
def fake_send(*args, **kwargs):
device.handle_state_update(**mock_v3_state)

with patch.object(Device, "send", wraps=fake_send()):
with patch.object(Device, "send", wraps=fake_send):
await device.update_state()
assert device.version is None

Expand All @@ -38,7 +38,7 @@ async def test_issue_87_quiet_should_set_2():
def fake_send(*args, **kwargs):
device.handle_state_update(**mock_v3_state)

with patch.object(Device, "send", wraps=fake_send()) as mock:
with patch.object(Device, "send", wraps=fake_send) as mock:
await device.push_state_update()
mock.assert_called_once()

Expand Down