Emis Web API GetPatientSearches on PatientAccessEMISWebV4_2 (em_pacc.dll) - looking for parameter info

Wondering if anyone on here has experience / info with the Emis Web Patient Access api? I’m specifically interested in the GetPatientSearches method of the PatientAccessEMISWebV4_2 COM object.

I’m working in python and can login, swap patients, retrieve and parse XML patient records, query them etc.

I’m trying to find a solution to the lack of a decent patient diary in Emis and having drawn a blank with the patient record (the diary entries don’t appear to change when completed), I’m wondering if this API call will allow me to retrieve a list of patients identified by a given search within Emis Web.

It’s a COM object and the signature for the method is:

HRESULT GetPatientSearches(
[in] BSTR sessionID,
[in] BSTR Guid,
[in] BSTR Site,
[in] BSTR ReportType,
[out] VARIANT* PatientList,
[out] VARIANT* error,
[out] VARIANT* outcome,
[out, retval] VARIANT* Value);

sessionID is a known, GUID I’m guessing is the search ID (there are a few GUIDs in searches when exported as XML so likely one of them), Site I’m guessing will be an Emis site code but ReportType??

Wondering if anyone has used this particular call (or has had more like finding documentation)?

As above, I’m hoping it will give me a list of patients identified by a given search in Emis Web.

Does this ring any bells for anyone?

Iain

I’ve not used it but I think thats for returning QOF data

You will get more information in the partner api specification, available from EMIS.

That would make sense (but if works would be nice to have access). Do you happen to know if the Partner API is online somewhere (haven’t been able to find), and if not, will Emis share with a humble GP without making me jump impractical hurdles?

Iain

1 Like

I couldn’t find anything online about the Partner API. I suspect that it’ll be an ‘Open API’ in name only, just like all the others in healthcare. Strikingly different from the ‘Open APIs’ rhetoric back around (I’m guessing) 2015 from EMIS, when agreeing to open APIs was a new requirement in the renegotiated GPSoC-R contract.

Really interesting that you are doing this - have you shared the code anywhere? eg GitHub?

Evening.

[Apologies re non linking links - original reply bounced because newbies can’t post a reply with more than 2 links. Also seem to have turned code comments into banner headlines - oh well]

Not on Github but potentially could be if I ever get it far enough to be (a) clean and (b) useful. Have included enough before to kickstart anyone interested in the same things. Agree that the lack of an “open” open api is frustrating but with a bit of stubbornness and few simple tools necessity is most definitely the mother of invention.

The tricky bit is the API key - can’t share working code even though 5 minutes with a debugger or similar (I like WinAPIOverride : Free Advanced API Monitor, spy or override API or exe internal functions) will find you one I would imagine. The key bit would be the InitialiseWithId call as below while an API app runs. Find that and you find the key… allegedly.

The basic idea is that em_pacc.dll contains COM objects (which oleview.exe from within even the free version of visual studio) will happily show. In Python, if you install the win32 extensions (pywin32 · PyPI) you get COM support and Quick Start to Client side COM and Python talks through how that works.

What I have working is - connect, get current patient id or list of changed records since a given date, retrieve record as XML file (be very, very, very careful with your data security if you do any of this), parse the XML (the lxml objectify layer is handy here) then wrap each Medication XML entry (or BP entry etc) in a data class which extracts useful info… which can finally be inserted into a sqlite database or reported in some other fashion. As an aside, the TRUD dm+d datasets are also XML and lxml does a lovely job mapping AMP through VMP to VTM such that one can say Zestoretic is lisinopril+hydrochlorothiazide (which is basically all you need to say “what is it and what monitoring does it need?”).

I’d hoped that the diary entries would change meaningfully on completion (but they don’t)… hence the query about retrieving searches from Emis. It’s odd… completing a diary entry seems to change a pathology result when you compare pre and post change XML… all very curious.

Some code for the curious below.

Iain

# COM will only work on windows

if sys.platform.startswith('win32'):

import win32com.client

from win32com.client import gencache

import pythoncom

# Define a namespace for the XML we'll be parsing

BNS = 'http://www.e-mis.com/emisopen/MedicalRecord'

#-------------------------------------------------------------------------------

@dataclass

class EMW():

""" This is Emis Web itself """

_emw: object = field(init=False, default=None, repr=False)

_SID: str = field(init=False, default=None, repr=False)

_ChangedMR: list = field(init=False,default=None,repr=False)

def __exit__(self, exc_type, exc_value, exc_traceback):

""" Tidy up gracefully """

if sys.platform.startswith('win32'): pythoncom.CoUninitialize()

# print ('EMW cleanup finished')

def __enter__(self):

""" Connect to Emis Web """

# COM will only work on windows

if sys.platform.startswith('win32'):

# This is EM_PACC.dll from EmisWeb

gencache.EnsureModule('{99847EE4-4F79-4EDD-A420-79487CD94EEF}', 0, 1, 0)

# As per http://www.icodeguru.com/WebServer/Python-Programming-on-Win32/appd.htm

sys.coinit_flags = 0 # pythoncom.COINIT_MULTITHREADED == 0

self._emw=win32com.client.Dispatch('EM_PACC.PatientAccess_20100519')

# Using the API key

print ('Initialising connection...', end='\r', flush=True)

(rv, cdb, product, version, loginID, error, outcome, self._SID)= self._emw.InitializeWithID(0, "[webinterop.cymru.nhs.uk](http://webinterop.cymru.nhs.uk/)", "", "",

"4 digit site code here", 'API key here in 8-4-4-12 hex digit GUID format')

if outcome != '4': raise AssertionError(f'InitializeWithID - Outcome code {outcome}')

print ('Initialising connection... done', flush=True)

#stream = pythoncom.CoMarshalInterThreadInterfaceInStream(pythoncom.IID_IDispatch, self._emw._oleobj_)

else:

print ('Not on windows - EMW.Connect run')

return self

- after that you’ve got things like

# -----------------------------------------------------------------------------

def ChangedMR_Patients(self, when='1860/01/01') -> list:

# Keep everyone informed

print ('Requesting list of patients with updated medical records...', flush=True)

# Request the list

(rv, pts, error, outcome)= self._emw.GetChangedPatientsMR(self._SID, when)

# print (f'Outcome: {outcome}, Error: {error}, RV: {rv}')

# Outcome = 3 when future date given

if outcome != '1': raise AssertionError(f'GetChangedPatientsMR - {error}')

# Save off the XML received

with bz2.open('GetChangedPatientsMR_XML.bz2', 'wt', encoding='utf-8') as f: f.write(pts)

# Parse the XML

d = etree.fromstring(pts)

# Build a patient list - ugly though namespaces are... it's faster to work with than to remove them

return [e for e in etree.XPath("""//x:PatientMatches/x:PatientList/x:Patient/x:DBID/text()""",namespaces={'x': BNS})(d)]

to get a list of EMIS patient IDs. Don’t seem to be able to specify a granularity of less than a day but 2021/06/14 would mean anything from 00:00 this morning onwards and

# -----------------------------------------------------------------------------

def Get_Medical_Record(self, pt_list):

# Patient list is a deck (deque) - basically a FIFO queue

try:

# COM will only work on windows

if sys.platform.startswith('win32'):

pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)

emw2 = win32com.client.Dispatch(self._emw)

while pt_list:

pid = pt_list.popleft():

start = time.perf_counter()

# print (f'Processing {pid}...')

# Get medical record

(rv,rec,error,outcome) = emw2.GetMedicalRecord(self._SID, pid)

#print (f'{rv},{error},{outcome}')

# Outcome 1 = medical record found

if outcome == '3':

raise ValueError(f'Patient {pid} not found')

else:

# Store the XML

with bz2.open(os.path.join('Data','MR',f'{pid}.xz2'), 'wt', encoding='utf-8') as f: f.write(rec)

print (f'Patient {pid} retrieved in {time.perf_counter() - start:.2f} seconds.')

finally:

# Release the COM object

pythoncom.CoUninitialize()

with a bit of COM glue thrown in because this runs asynchronously in a set of threads all working through the same list (can take 4-16 seconds to retrieve a record so why not grab 10 in parallel). I’m a bit wooly on COM so some of this may be overkill.

Once you’ve got an XML file (the medical record) then the python bindings for the gloriously fast lxml gives you

with bz2.open(f'Data/MR/{pid}.xz2','r') as f:

# Parse the XML

MR = objectify.parse(f)

xpe = etree.XPathEvaluator(MR,namespaces={'x': BNS})

on a bzip2 compressed XML file and then

# Update Medications table - delete old entries then (re)populate

db.execute('delete from Medications where pid = ?', (pid,))

db.executemany('INSERT INTO Medications VALUES (?,?,?,?)',

((pid,m.last_issued,m.code,m.term) for m in (Medication(e) for e in xpe('/x:MedicalRecord/x:MedicationList/x:Medication'))))

which uses generator comprehensions (sort of nested for loops). The Medication bit starts:

@dataclass

class Medication:

_e : objectify.ObjectifiedElement = field(init=False, repr=True)

when: date = field(init=False, repr=False)

def __init__(self,e): self._e = e

@property

def when(self) -> date:

try:

w = str(self._e.AssignedDate)

return date(int(w[-4:]),int(w[3:5]),int(w[:2]))

except AttributeError: return None

Cool. I’ll have a read through more soon. But for now I edited your post to make the code nicely formatted.
For info:
Code blocks - start with
```python
End with
```
And everything in between will be formatted properly

BTW I found some smart quotes (“ and ‘) in the code which could have been from when they were copy-pasted - some programs will automatically put smart quotes in but this breaks programs. So it may be nothing, but if you have a weird bug it is likely those quotes!

I would consider using the new Terminology servers - NHS Digital for this. That should remove the need to process the TRUD files. dm+d is currently SNOMED Concepts on this server but will be changing to a new system (same/similar codes but not SNOMED).

My code is roughly the same but I’m using java. I’ve created XML schema files from the XML and then used those schemas to create java classes.

EMIS Diary Entries are simple FHIR Tasks Task - FHIR v4.0.1 and I don’t think they link to other objects. (May be a link from these Tasks to Appointments but I’ve not found one). I think you’re correct that a search is the best way of identifying but I don’t think you can do that with the API. Its a shame you can’t do this, you used to be able to send searches to GP systems (MiQuest) but the ‘modern’ approach is to grab all the data and query centrally (and that is in the news care.data/gp.data)

We don’t store any of the data (maybe some rules that say you can’t store the data?), we instead turn the XML objects into FHIR objects and expose those to the developers. So a list of all events is a FHIR GET /Observation?patient={emis_id}
Medications is GET /MedicationRequest?patient={emis_id}
etc.

Fab - thanks for the reformat.

Useful pointers here… have to have a look at FHIR and agree re storing data locally. My code so far is purely proof of concept… just so much easier to see and explore patterns using SQL than (bleuch) XML :slight_smile.

Looks like no joy so far with finding parameters to that API call as agree that getting Emis to do the heavy lifting so I can just grab some output (even if just a list of patient IDs) would be best.

Cheers for everything so far.

Don’t suppose anyone on here has access to partner API documentation?

Partner API includes a confidentiality agreement (I think)

Check the SLA on the NHS Digital Terminology Servers - there are no guarantees that these will be there when you need them …

Partner API does include a confidentiality agreement

1 Like

Would that extend to format of sample parameters for a single API call?

1 Like

Sadly I suspect so. The way the GP system suppliers have approached ‘open’ APIs is to have closed APIs with no public documentation, protected by ‘Confidentiality Agreement’ (ie NDA), and unfortunately those in charge of enforcing GPSoC-R (the purchasing framework for GP systems) and subsequently GP IT Futures have not called them out on it.

Well that’s somewhat annoying even if entirely expected.

So… let’s try a different tack then.

In Emis Web, does anyone know of a functional diary management mechanism / standalone app?

Imagine a common clinical scenario - I have a patient with impaired renal function that I’m keeping an eye on (they might be new to me so all I’ve got is that creatinine of 130). I’d like to be able to say, I’d like you to have your renal function checked every 3m for a year so we can establish your baseline / progression rate / need for intervention.

In EmisWeb, I can add the first test as diary entry using a code of my choosing but I can’t book subsequent tests because they don’t allow 2 diary entries with the same read/snomed code.

In 3m, the blood tests is done and I can capture the IFTT bit with a concept tested by a protocol for that specific code and I could complete the diary entry relatively automatically.

What I can’t do is see that the diary said “Clin: Creat 130, 3m check | Test: UEC | Recurrence: 4 tests @ 3m intervals starting 16.6.21” so… I complete the diary and the recurrence rule is lost.

I could work round that by having a diary or a tickle file or whatever for those repeat tests… but really??? And what about the FBC, the PSA, the BP, the weight… all of which need individual concepts.

Where’s the mechanism to say “oh, you know those bloods you’ve just seen… here are some diary entries with matching codes… do you want to complete any of them? Oh, and that one for the UEC… that’s a recurring one… shall I book the next test?”

I could do this in Outlook (with some form of patient anonymisation) but it’s not the most robust solution or even with a paper diary… but it’s screaming out for automation… what’s due today/next week for this patient or across the practice? Who has diary entries we’re not managing to complete (because they keep not booking the phlebotomy appointment), how many spirometries do we need capacity for (when such things become practical again)?

I’d hoped that getting access to the raw record (in XML) would help - I can read the diary entry… I can see the blood tests… I could generate a list of diary entries we could complete (and those which need rebooked)… but, as far as I can tell, the MedicalRecord/DiaryList/Diary xml nodes remain unchanged when the diary entry is completed in EmisWeb. In fact, the only thing which seems to change in the whole record is the timestamp on the data extract. So I’d end up listing completed entries for re-completion for ever more.

That’s what brought me to that API call. If I can just find some clue to what should be in the 2nd and 3rd parameter (and it really is the 3rd one primarily), maybe, just maybe I could use a search for incomplete diary entries as that seems to work.

Any cunning plans anyone?

Oh, should add… yes, have searched on the EmisWeb support forums.

I think you need a separate Task management tool?
Probably it periodically exports patients (however extracting your entire practice seems very wrong).

What I can share (as HL7 FHIR is an open standard). This is an example of diary entry sourced from a primary care system


  {
                "resourceType": "Task",
                "id": "6457498",
                "identifier": [
                    {
                        "system": "https://xxx.com/Id/Diary/DBID",
                        "value": "6457498"
                    },
                    {
                        "system": "https://xxx.com/Id/Diary/GUID",
                        "value": "A5EC8BCB-FF13-4703-A4C4-DCEAB9941CE9"
                    }
                ],
                "code": {
                    "coding": [
                        {
                            "system": "http://read.info/readv2",
                            "code": "65E..",
                            "display": "Influenza vaccination"
                        },
                        {
                            "system": "http://snomed.info/sct",
                            "code": "86198006",
                            "display": "Influenza vaccination"
                        }
                    ],
                    "text": "Influenza vaccination"
                },
                "for": {
                    "reference": "Patient/6457"
                },
                "executionPeriod": {
                    "start": "2006-10-21T00:00:00+00:00"
                },
                "requester": {
                    "reference": "Practitioner/50"
                }
            }

I can’t see anything there which tells me the Task has been completed. I could probably use SNOMED code to search the medical record to find a matching Immunization

e.g.

{
                "resourceType": "Immunization",
                "id": "645767",
                "identifier": [
                    {
                        "system": "https://XXX.com/Id/Event/DBID",
                        "value": "645767"
                    },
                    {
                        "system": "https://XXX.com/Id/Event/GUID",
                        "value": "1547C72F-EF2A-4070-B49F-D53727105458"
                    }
                ],
                "status": "completed",
                "vaccineCode": {
                    "coding": [
                        {
                            "system": "http://read.info/readv2",
                            "code": "65ED.",
                            "display": "Seasonal influenza vaccination"
                        },
                        {
                            "system": "http://snomed.info/sct",
                            "code": "822851000000102",
                            "display": "Seasonal influenza vaccination"
                        }
                    ],
                    "text": "Seasonal influenza vaccination "
                },
                "patient": {
                    "reference": "Patient/6457"
                },
                "occurrenceDateTime": "2014-02-07T00:00:00+00:00",
                "performer": [
                    {
                        "actor": {
                            "reference": "Practitioner/35"
                        }
                    }
                ]
            }

The dates are a bit off though.

So you could infer it has been completed but may a separate tool where you manage the diary entries as tasks. Write back task completion codes back to EMIS?

Hi Kev - that’s very useful and I think an external tool may be the only realistic way to go.

Looking at the XML samples, that’s remarkably similar to what I see in the XML from the API and I agree wholeheartedly that a mass export of patient data is a scary minefield. Without a detectable change in the diary entries (an entry completed flag of some sort) however, not sure how else to solve.

Best I’ve got to would be one of 5 options:

(1) Within Emis, do much as you suggest (and I currently do) i.e. for each diary entry of interest, write a search to look for a matching immunisation/blood test/BP etc and then manually use those 20 or so searches to complete entries to maintain the diary.

The advantage is that all is internal to Emis but it’s a cumbersome beast to build, maintain and defeats much of the advantages of automation. Best I’ve got so far is - here’s a folder with a whole bunch of diary based tests, tell me which patients are in each list or which patients are on at least one list (even if you lack a mechanism to tell me which lists those are).

(2) Again with Emis, try and write a search which says “For ANY read/snomed coded diary entry, give me a list of patients with a same coded entry on or after that date” i.e. one search covering all diary entries. Haven’t managed to create such a search as yet but stubborness is the mother of something or other.

Would be a cleaner version of option 1 but still only gets me to “Human, go look at this list of patients!”. Hardly ticks the computer as servant box.

(3) Use the API to generate a list of patients with changed records (might be new diary, might be new test result etc), retrieve the XML record and do the IFTT test to see if a diary entry might be ready to complete and which one it might be. After that, add to an actions list to pass to someone to update clinical system.

Could all happen in memory i.e. no extracted data persistence other than the actions list (which would be for this patient ID, diary entries saying XXX look ready for completion).

This wouldn’t need all existing diary entries to be held externally - the API can identify changed records and could work with “something has changed… up to you dear coder to see what”. Could either look at each diary entry and see for the latest of each code (they overwrite), and see whether that entry looks satisfied or if persisting the last XML medical record outside of Emis were possible, change enumeration is trivial (deepdiff · PyPI for the pythonistas in the audience)

(4) Use the API, extract all current diary entries and store them in some secure format externally (essentially equivalent to creating Outlook diary tasks… and no, I wouldn’t either) and then with each change in the record (as per 3 above), test again to see if could complete.

This has the advantage of maintaining a diary which can be manipulated / printed but is otherwise a bit of a nightmare.

For both 3 and 4, FHIR looks like it would tick the same or similar boxes but is another bit of new learning.

(5) This one is dependent on finding working parameters to the API call with which I started this thread and, assuming it basically allows retrieval of the patient list from a GUID identified Emis Web search…

Use the searches I already have but use python or Rust or name your COM talking language to do the smart set combinations to be able to say “Having looked at your 100 individual searches across your patient population which we ran on Sunday night, your patient needs their renal function checked because they’re on 6 lists of “Needs UEC because…” and their last test was >3m ago… oh, and by the way the trend on their creatinine is upwards at greater than a 5 degree angle with a significantly raised baseline so perhaps do them sooner rather than later?”

If could get report output (i.e. values lists) by the same mechanism then Robert would indeed be my uncle and I would be happy.

I know nobody can but a private message with a cut and paste from the relevant bit of the docs would be lovely :slight_smile:

Just wondering, if anyone on the list does have access to the partner API… even assuming Emis would accept me as a bonafide non commercial developer, I take it there’s a hefty toll in sheckels of gold to accompany the priviledge?

It’s probably useful to start the conversation with EMIS.

From a technical point of view - you need something like a Task event notification. So when any Task (diary entry) is created or updated, you get notified.

EMIS may have this - it might not be an externally facing service though.