Skip to content

Commit 2500f6d

Browse files
committed
Fixed unit tests & added temperature unit tests
1 parent 2320237 commit 2500f6d

3 files changed

Lines changed: 165 additions & 1 deletion

File tree

myolink/device/hand.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ def _control_notification_handler(self, sender_handle: int, data: bytearray):
293293
try:
294294
humidity_value = struct.unpack(">f", response_payload[:4])[0]
295295
if math.isnan(humidity_value) or math.isinf(humidity_value):
296-
logger.error(f"[{self.address}] Received invalid humidity float ({humidity_value}) for CMD 0x{cmd_id_resp:02X}. Payload: {response_payload[:4].hex()}")
296+
logger.error(f"[{self.address}] Received invalid humidity float value ({humidity_value}) for CMD 0x{cmd_id_resp:02X}. Payload: {response_payload[:4].hex()}")
297297
future_for_cmd.set_exception(HandCommandError(f"Received invalid humidity float value: {humidity_value}", status=response_status, raw_response=data))
298298
else:
299299
logger.info(f"[{self.address}] Parsed humidity: {humidity_value:.2f}% for CMD 0x{cmd_id_resp:02X}")

raise

Whitespace-only changes.

tests/test_hand_commands.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,167 @@ async def test_NotificationHandler_ShouldSetException_ForUnknownResponseStatus(h
598598

599599
if cmd_id in hand_instance._pending_command_futures:
600600
del hand_instance._pending_command_futures[cmd_id]
601+
602+
@pytest.mark.asyncio
603+
async def test_NotificationHandler_ShouldSetResultWithTuple_ForSuccessfulHumidityTempResponse(hand_instance):
604+
"""Verify handler sets result with (humidity, temperature) tuple for 8-byte response."""
605+
# Simulate a successful humidity and temperature notification (8 bytes payload)
606+
humidity_value = 55.5
607+
temperature_value = 22.3
608+
humidity_bytes = struct.pack(">f", humidity_value)
609+
temperature_bytes = struct.pack(">f", temperature_value)
610+
payload = humidity_bytes + temperature_bytes
611+
# Header: Schema | CMD_ID | Status (Success=0, IsRequest=0) | Length
612+
header = struct.pack(">BBBB", SCHEMA_VERSION, CMD_GET_RELATIVE_HUMIDITY, 0x00, len(payload))
613+
simulated_data = bytearray(header + payload)
614+
sender_handle = 33 # Dummy handle
615+
616+
# Create and register a future
617+
humidity_temp_future = asyncio.Future()
618+
hand_instance._pending_command_futures[CMD_GET_RELATIVE_HUMIDITY] = humidity_temp_future
619+
620+
with patch('myolink.device.hand.logger') as mock_logger:
621+
# Call the handler
622+
hand_instance._control_notification_handler(sender_handle, simulated_data)
623+
624+
# Assert the future is done and has the correct result (tuple)
625+
assert humidity_temp_future.done(), "Humidity/Temperature future should be done."
626+
assert not humidity_temp_future.cancelled(), "Humidity/Temperature future should not be cancelled."
627+
assert humidity_temp_future.exception() is None, f"Humidity/Temperature future should not have an exception, but got {humidity_temp_future.exception()}."
628+
result = humidity_temp_future.result()
629+
assert isinstance(result, tuple), "Result should be a tuple."
630+
assert len(result) == 2, "Result tuple should have two elements."
631+
assert isinstance(result[0], float), "First element should be float (humidity)."
632+
assert isinstance(result[1], float), "Second element should be float (temperature)."
633+
assert result[0] == pytest.approx(humidity_value), "Humidity value should match."
634+
assert result[1] == pytest.approx(temperature_value), "Temperature value should match."
635+
636+
# Check log message
637+
mock_logger.info.assert_any_call(
638+
f"[{hand_instance.address}] Parsed humidity: {humidity_value:.2f}%, Temperature: {temperature_value:.2f}°C for CMD 0x{CMD_GET_RELATIVE_HUMIDITY:02X}"
639+
)
640+
641+
642+
@pytest.mark.asyncio
643+
async def test_NotificationHandler_ShouldSetException_ForHumidityTempInvalidFloatData(hand_instance):
644+
"""Verify handler sets exception for 8-byte response with invalid float data."""
645+
# Simulate an 8-byte successful response with invalid float bytes (e.g., NaN for temp)
646+
humidity_value = 55.5
647+
invalid_temp_bytes = b'\x7f\xc0\x00\x00' # NaN float bytes
648+
humidity_bytes = struct.pack(">f", humidity_value)
649+
payload = humidity_bytes + invalid_temp_bytes
650+
# Header: Schema | CMD_ID | Status (Success=0, IsRequest=0) | Length
651+
header = struct.pack(">BBBB", SCHEMA_VERSION, CMD_GET_RELATIVE_HUMIDITY, 0x00, len(payload))
652+
simulated_data = bytearray(header + payload)
653+
sender_handle = 33 # Dummy handle
654+
655+
# Create and register a future
656+
humidity_temp_future = asyncio.Future()
657+
hand_instance._pending_command_futures[CMD_GET_RELATIVE_HUMIDITY] = humidity_temp_future
658+
659+
with patch('myolink.device.hand.logger') as mock_logger:
660+
# Call the handler
661+
hand_instance._control_notification_handler(sender_handle, simulated_data)
662+
663+
# Assert the future is done and has a HandCommandError exception
664+
assert humidity_temp_future.done(), "Humidity/Temperature future should be done after invalid float data notification."
665+
assert not humidity_temp_future.cancelled(), "Humidity/Temperature future should not be cancelled."
666+
assert humidity_temp_future.exception() is not None, "Humidity/Temperature future should have an exception."
667+
assert isinstance(humidity_temp_future.exception(), HandCommandError), "Exception should be HandCommandError."
668+
assert "Received invalid humidity/temperature float values" in str(humidity_temp_future.exception()), "Error message should mention invalid float values."
669+
670+
# Check for error log about invalid float values
671+
mock_logger.error.assert_any_call(
672+
f"[{hand_instance.address}] Received invalid humidity/temperature float values for CMD 0x{CMD_GET_RELATIVE_HUMIDITY:02X}. Payload: {payload.hex()}"
673+
)
674+
675+
676+
@pytest.mark.asyncio
677+
async def test_GetTemperature_ShouldSendCorrectCommandAndReturnTemperature(hand_instance, mock_bleak_client):
678+
"""Verify get_temperature sends the correct command and returns temperature from 8-byte response."""
679+
# Expected command is the same as get_relative_humidity
680+
control_byte_request = 0x01
681+
data_length = 0x00
682+
expected_command_packet = struct.pack(">BBBB", SCHEMA_VERSION, CMD_GET_RELATIVE_HUMIDITY, control_byte_request, data_length)
683+
684+
# Simulate the _send_command_and_process_response returning an 8-byte response tuple
685+
humidity_value = 60.0
686+
temperature_value = 25.0
687+
simulated_result = (humidity_value, temperature_value)
688+
689+
# Patch _send_command_and_process_response to return the simulated result
690+
with patch.object(hand_instance, '_send_command_and_process_response', new_callable=AsyncMock) as mock_send_command: # Removed mock_ensure_notify
691+
692+
mock_send_command.return_value = simulated_result
693+
694+
# Call get_temperature
695+
temperature = await hand_instance.get_temperature(timeout=1.0)
696+
697+
# Assert _send_command_and_process_response was called with correct arguments
698+
mock_send_command.assert_awaited_once_with(
699+
command_id=CMD_GET_RELATIVE_HUMIDITY,
700+
request_payload=b'',
701+
timeout=1.0
702+
)
703+
704+
# Assert the correct temperature was returned
705+
assert temperature == pytest.approx(temperature_value), "get_temperature should return the correct temperature."
706+
707+
708+
@pytest.mark.asyncio
709+
async def test_GetTemperature_ShouldReturnNone_For4ByteResponse(hand_instance, mock_bleak_client):
710+
"""Verify get_temperature returns None when handler provides 4-byte (humidity only) response."""
711+
# Simulate the _send_command_and_process_response returning a 4-byte response (float)
712+
humidity_value = 60.0
713+
simulated_result = humidity_value
714+
715+
# Patch _send_command_and_process_response to return the simulated result
716+
with patch.object(hand_instance, '_send_command_and_process_response', new_callable=AsyncMock) as mock_send_command, \
717+
patch('myolink.device.hand.logger') as mock_logger: # Removed mock_ensure_notify
718+
719+
mock_send_command.return_value = simulated_result
720+
721+
# Call get_temperature
722+
temperature = await hand_instance.get_temperature(timeout=1.0)
723+
724+
# Assert _send_command_and_process_response was called correctly
725+
mock_send_command.assert_awaited_once_with(
726+
command_id=CMD_GET_RELATIVE_HUMIDITY,
727+
request_payload=b'',
728+
timeout=1.0
729+
)
730+
731+
# Assert None was returned
732+
assert temperature is None, "get_temperature should return None for 4-byte response."
733+
734+
# Check for the warning log
735+
mock_logger.warning.assert_any_call(
736+
f"[{hand_instance.address}] Received humidity only response (4 bytes), temperature not available."
737+
)
738+
739+
740+
@pytest.mark.asyncio
741+
async def test_GetRelativeHumidity_ShouldReturnHumidityFrom8ByteResponse(hand_instance, mock_bleak_client):
742+
"""Verify get_relative_humidity returns humidity from an 8-byte response tuple."""
743+
# Simulate the _send_command_and_process_response returning an 8-byte response tuple
744+
humidity_value = 65.0
745+
temperature_value = 23.5
746+
simulated_result = (humidity_value, temperature_value)
747+
748+
# Patch _send_command_and_process_response to return the simulated result
749+
with patch.object(hand_instance, '_send_command_and_process_response', new_callable=AsyncMock) as mock_send_command: # Removed mock_ensure_notify
750+
751+
mock_send_command.return_value = simulated_result
752+
753+
# Call get_relative_humidity
754+
humidity = await hand_instance.get_relative_humidity(timeout=1.0)
755+
756+
# Assert _send_command_and_process_response was called correctly
757+
mock_send_command.assert_awaited_once_with(
758+
command_id=CMD_GET_RELATIVE_HUMIDITY,
759+
request_payload=b'',
760+
timeout=1.0
761+
)
762+
763+
# Assert the correct humidity was returned
764+
assert humidity == pytest.approx(humidity_value), "get_relative_humidity should return the humidity from an 8-byte response."

0 commit comments

Comments
 (0)