@@ -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