If you look at the Bluetooth assigned 16-bit UUIDs numbers then you are correct that 0x180D
is the Heart Rate service

It will be the Heart Rate Measurement Characteristic 0x2A37
that will have the values you are seeking:

I don't have a device to test this with and I don't know what platform you are writing the code for.
As a result I've used the bleak library as that is the most cross platform library.
The example also subscribes to notifications from the device rather than reads the value. This is a more typical way to get values that are regularly updating.
import asyncio
import bitstruct
import struct
from bleak import BleakClient
HR_MEAS = "00002A37-0000-1000-8000-00805F9B34FB"
async def run(address, debug=False):
async with BleakClient(address) as client:
connected = await client.is_connected()
print("Connected: {0}".format(connected))
def hr_val_handler(sender, data):
"""Simple notification handler for Heart Rate Measurement."""
print("HR Measurement raw = {0}: {1}".format(sender, data))
(hr_fmt,
snsr_detect,
snsr_cntct_spprtd,
nrg_expnd,
rr_int) = bitstruct.unpack("b1b1b1b1b1<", data)
if hr_fmt:
hr_val, = struct.unpack_from("<H", data, 1)
else:
hr_val, = struct.unpack_from("<B", data, 1)
print(f"HR Value: {hr_val}")
await client.start_notify(HR_MEAS, hr_val_handler)
while await client.is_connected():
await asyncio.sleep(1)
if __name__ == "__main__":
address = ("xx:xx:xx:xx:xx:xx") # Change to address of device
loop = asyncio.get_event_loop()
loop.run_until_complete(run(address))
You can read more about the Heart Rate Measure value is constructed in:
https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-6/