Emacs
is more than my favorite text editor, it is building material. As such,
I use it for everything I can, it’s amazing how it always steps up to the
challenge!
The internet revived the postal service instead of killing it. Today, I receive
more packages than ever before. Tracking where they are doesn’t however fit my
workflow. It requires me to open websites, input the tracking id, or at least
search for the email with the link. Thus I wondered: Is there a better way to do
this in Emacs?
Org-mode
is great to track of tasks, it provides an agenda view, it is a
workflow I like and it was already the way I kept track of packages. I would
create a new entry, have a link to the email with the tracking information. I
read my email using notmuch
’s Emacs
frontend. Then I would click on the link
in the Email, which opens the browser. That is a lot of clicks.
How about the org entry has all the tracking information and I can update it on
the spot?
Of course! That shouldn’t be a challenge at all. I opened the tracking website.
In this case is DHL Germany
and looking at the network requests on my browser
I found a JSON
endpoint which has the tracking information. What a great luck!
The problem is now easy to solve: query the endpoint, parse the response, update
the org-entry
.
DHL Germany
gives tracking information for unauthenticated users, over a
simple GET
request at the url:
https://www.dhl.de/int-verfolgen/data/search?piececode={track-id}&noRedirect=true&language=en
.
Simply replace {track-id}
with your shipment tracking number and done.
I must clarify, that this in not an official service endpoint, it is a hack. I
figured out by looking at their network requests of the tracking webpage. The
webpage and this endpoint don’t give all the information. You must first input
on the website your postal code to authenticate yourself and get detailed
information. I, however, couldn’t figure out how that request works. Since the
information I get from this url
is good enough for my purposes, I didn’t try
any harder, not did I tried the official way using DHL
’s API
and getting an
and account and an API
key and all that drama.
The Emacs
function for the request is:
1(defun delivery-track--dhl-de (track-id)
2 "Async request to dhl API using TRACK-ID.
3Write update on the org-node in current buffer."
4 (let* ((url-request-method "GET")
5 (params `((piececode ,track-id)
6 ("noRedirect" true)
7 (language "en"))))
8 (thread-first
9 "https://www.dhl.de/int-verfolgen/data/search?"
10 (concat (url-build-query-string params))
11 (url-retrieve #'delivery-track--process-response
12 (list (current-buffer) #'delivery-track-entry--dhl)))))
Next is an auxiliary function to parse the JSON
response and pass that to a
dedicated function which extracts the necessary data. Once the data is in the
appropriate format, it goes again to the function that updates the org entry.
1(defun delivery-track--process-response (_request-status buffer provider-parser)
2 "Process API response with PROVIDER-PARSER and update into BUFFER."
3 (goto-char url-http-end-of-headers)
4 (apply #'delivery-track-org-entry
5 buffer
6 (funcall provider-parser (json-parse-buffer))))
Let us focus then on the provider-parser
argument which in our case is the
function given earlier delivery-track-entry--dhl
. It processes the response
object to return a 3 element list, having the tracking ID
again, the current
status and a list of timestamped events.
1(defun delivery-track-entry--dhl (response)
2 "Parse DHL RESPONSE into standard delivery-track info for writer."
3 (let* ((shipment-info (seq-find
4 (lambda (item)
5 (eq t (gethash "hasCompleteDetails" item)))
6 (gethash "sendungen" response)))
7 (history (thread-last
8 shipment-info
9 (gethash "sendungsdetails")
10 (gethash "sendungsverlauf"))))
11 (list
12 (gethash "id" shipment-info)
13 (gethash "kurzStatus" history)
14 (thread-last
15 (gethash "events" history)
16 (seq-keep (lambda (event)
17 (when-let ((time (gethash "datum" event)))
18 (list time (gethash "status" event)))))
19 (reverse)))))
This is where it gets ugly. I’m finally mutating state and editing the org
buffer. delivery-track-org-entry
must first find which org-entry
to update.
It does so by going to the buffer where the entry should be, and finding it by
id
. It is a design decision to make the shipping/tracking number the id
of
the entry, that way I can use standard org functions to find the entry. That all
happens in lines 11-12
.
1(defun delivery-track-write-time! (timestring)
2 "Insert an org inactive timestamp from a parse-able TIMESTRING."
3 (thread-first
4 timestring
5 (parse-time-string)
6 (encode-time)
7 (org-insert-timestamp t t)))
8
9(defun delivery-track-org-entry (buffer shipment-id status events)
10 "Write update on BUFFER the SHIPMENT-ID with STATUS and EVENTS."
11 (with-current-buffer buffer
12 (goto-char (org-find-entry-with-id shipment-id))
13 (org-entry-put nil "status" status)
14 (org-next-visible-heading 1)
15 (seq-let (level _rlevel _todo _prio headline)
16 (org-heading-components)
17 (when (and (= level 2)
18 (string= "Shipment reverse history" headline))
19 (org-cut-subtree))
20
21 (insert "** Shipment reverse history\n")
22 (seq-doseq (event events)
23 (seq-let (time status) event
24 (delivery-track-write-time! time)
25 (insert " " status "\n"))))))
Then it is all about changing things. First, I update the property status
(line 13
). Then move forward one heading (line 14
). I check if that heading
is the subheading containing the shipping history (lines 15-19
). If it is, I
delete all its contents. If not, I don’t do anything. This is nice, because as
long as I use the first sub-tree to be the shipping history, I can write some
notes about the packaging before, and also some other subtrees of information.
Lastly, I write in place the shipping history, every event on the list (lines
21-25
).
I only need to create an org entry place and set the property to the tracking number.
I additionally write some notes about the shipment, including the org-notmuch links
to the emails containing purchase information and the shipment information.
The shipment history gets updated with the previous function.
1* DONE That package
2:PROPERTIES:
3:ID: 00340434XXXXXXXXXXXX
4:status: Delivery successful.
5:END:
6[[notmuch:id:[email protected]][from Store: Thanks for your purchase]]
7[[notmuch:id:[email protected]][from Store: Your shipment is on the way]]
8
9** Shipment reverse history
10[2024-09-26 Thu 14:34] The shipment has been successfully delivered
11[2024-09-26 Thu 08:08] The shipment has been loaded onto the delivery vehicle
12[2024-09-26 Thu 05:54] The shipment has been processed in the delivery base.
13[2024-09-25 Wed 23:23] The shipment arrived in the region of recipient and will be transported to the delivery base in the next step.
14[2024-09-25 Wed 18:46] The instruction data for this shipment have been provided by the sender to DHL electronically
15[2024-09-25 Wed 14:11] The shipment has been processed in the parcel center of origin
A convenient way to use this is creating an interactive function which I can
call over the entry.
1(defun delivery-track-update (track-id)
2 "Update tracking information for TRACK-ID.
3Interactive defaults to current buffer's org-node id and provider properties."
4 (interactive (list (read-string "What is the tracking id? " (org-id-get))))
5 (delivery-track--dhl-de track-id))
I can now have this information in my org-agenda
as a reminder. Especially if
I schedule the date of arrival.
By using org-columns
you can view a summary of your shipments. You can call
directly that function, or as I show next use a dynamic block.
1#+COLUMNS: %TODO %ITEM %ID %status
2# Call org-columns to view summary
3
4#+BEGIN: columnview :maxlevel 1 :id global
5| TODO | ITEM | ID | status |
6|------+--------------+----------------------+----------------------|
7| NEXT | Document | RR70479XXXXDE200 | |
8| DONE | Post | 54504XXXXXXX | Delivery successful. |
9| DONE | Vitamins | 3387241124XXXXXXX | |
10| DONE | Candy | 23353XXXXXXX | Delivery successful |
11| DONE | That package | 00340434XXXXXXXXXXXX | Delivery successful. |
12#+END:
This is a great tool. I reminds me why I like Emacs
, it reminds me that I
never need to conform to the way others do things and suffer under their User
Interfaces, I can always do it my way.
You can look at all the code in my publicly available dotfiles, specifically
this file: delivery-track.el
. As of today, it can also track packages by
Hermes
.
I wanted to deal with packages by DPD
, yet their website does not use a JSON
endpoint. Their backend already produces a webpage with all the tracking
information. I didn’t want to deal with scraping that page, but maybe you will.
I’m happy to read about it, if you do.