Tuesday, September 24, 2024

Withings HealthMate on iOS

Moin! :-)

In the year 2020 I did an analysis on the HealthMate App of Withings on Android devices.

I decided that it is time to do the same for the corresponding app on iOS devices.

For my last posts I always wrote a parse for ALEAPP - because they were about Android or Apps on Android. So this time I also wrote a parser for iLEAPP (Github Link)  - PR is created - should be merged soon.

In my experience there is always a lot of useful data stored in health apps. Data such like Activities (when, where, how intense), general movements, health status and and and...

My test device for this was an Apple iPhone 12 mini with iOS 17.5.1 installed. I used a Full File System Acquisition for my analysis.

The app is called: Withings Health Mate App (com.withings.wiScaleNG)

The app syncs the data from the devices to the Withings servers. I use the app and some of the Withings devices since 2019.

The Withings devices I use/used are the scale BodyComp and the watches ScanWatch, ScanWatch 2 and ScanWatch HR.

The data you can get out of the app is dependent on what data you track and what your devices can measure.


The following data can be found in general and are discussed in this post:

  1. Account info
  2. Messages (between users that are connected to each other)
  3. Devices connected to the account 
  4. Measurements
    • Automatic/Cyclic measurements made by the device (e.g. steps, heart rate, location, SPO2, temperature)    
  5. Tracking / Activities
    • Specific activities tracked manually or detected automatically (e.g.  Cycling, Swimming, Running)  

I only look at the data stored in the app. I won't take a look onto my Withings devices or into the data on the provider site.


Where is the data stored?

In the corresponding Sandbox Path of the App the interesting data is stored in the Folder "Library/Application Support" and its subfolders.

I will give the exact location in the sandbox path to every file in the corresponding section below.


Account Info

The used account of the app can be found in a JSON file.

Path: %SandboxPath%/Library/Application Support/
Name: account

You can get the following info out of the file:
  1. User ID
  2. First Name
  3. Last Name
  4. Short Name
  5. Birthdate (Stored as Apple Cocoa Core Data Timestamp - Local Time))
  6. E-Mail
  7. Creation Date (Stored as Unix Epoch Timestamp - UTC)
  8. Last Modified Date (Stored as Unix Epoch Timestamp - UTC)

Additionally there is stored info about the used security mechanisms (Password/biometric), the used auhtentication tokens and some other stuff.

Messages

In the app it is possible to write messages to other users. To do this the users need to be connected. Connected means in the app that they are competing against each other.
As far as I know there are no group chats possible at the moment.

The message data ist stored in a SQLITE database.

Path: %SandboxPath%/Library/Application Support/coredata
Name: [User ID]_HM3Timeline.sqlite

Table of interest: ZHMTIMELINEEVENT.

In the field ZTYPE the value gives info what type of message it is. Message from users have the value "HMTimelineMessageEvent".
System messages, e.g. if a measurement was above normal level, the value is "HMTimelineTextEvent".
I am interested in the user generated messages now, so I focus on the first value here.

The other interesting fields in the table are:


FieldMeaningAdditional info
ZUSERIDUser ID of the account in the app8-digit
ZSENDERIDUser ID of the sender of the message8-digit
ZRECEIVERIDUser ID of the receiver of the message8-digit
ZSENDERLASTNAMELast name of the sender
ZSENDERFIRSTNAMEFirst name of the sender
ZDATETimestamp of the message Apple Cocoa Core Data Timestamp
In Local Time
ZWSMODIFIEDDATELast modification date of the messageApple Cocoa Core Data Timestamp
In UTC
ZEXPIRATIONDATESet expriation date of the messageFunction not tested yet. In my data there were still messages in the database after the expiration date. Could be just relevant for the UI.
ZTYPEMESSAGEType of the messageFour values in my data:
- Cheer
- Taunt
- Custom
- Message
ZMESSAGE2Content of the message


Devices

Info to connected devices are stored in the following SQLITE database

Path: %SandboxPath%/Library/Application Support/coredata
Name: associated_devices.sqlite

Table of interest: ZWTDEVICE

Interesting fields:

FieldMeaningAdditional info
ZDEVICE_IDInternal Device ID8-digit
ZUSERIDUser ID of the sender of the message8-digit
ZCREATEDDate of the association of the device with the accountUnix Epoch Timestamp
In UTC
ZLASTCONNECTIONLast connection/sync with the deviceApple Cocoa Core Data Timestamp
In UTC
ZLAST_WEIGHINLast weigh in on the device - the last time the device got dataApple Cocoa Core Data Timestamp
In UTC
ZMACMAC address of the device
ZFIRMWAREFirmware version of the device at the last sync
ZLATITUDELatitude value of last syncNot too precise - dependent on the device and type of sync (via BT to App or via W-LAN)
ZLONGITUDELongitude value of last syncNot too precise - dependent on the device and type of sync (via BT to App or via W-LAN)
ZTIMEZONE

Timezone set on the device

Format: "Europe/Berlin"

ZISSYNCDISABLED

0 = Sync enabled
1 = Sync disabled




Measurements

I use the term measurement for the values that are captured periodically and automatically by the app.


Path: %SandboxPath%/Library/Application Support/coredata
Name: [User_ID]_vasistas.sqlite

Table of interest: ZVASISTAS

In the table the field ZCATEGORY stores info about the category of the data. Dependent on the value in this field different other fields are filled with the data. 
Please understand, that the values in ZCATEGORY are the ones in my data. It is possible, that there exist other values in the field ZCATEGORY, if other measurements are taken from other devices.


Overview of the interesting fields:

FieldMeaningAdditional info
ZCATEGORYCategory of the data measured:
0 = Steps
2 = Heart rate
3 = ??
5 = Location
6 = SPO2
8 = ??
12 = Body temperature
For the values 3 and 8 I, at the moment, don't have any idea what they mean.

ZDEVICEIDDevice ID of the device that measured the dataCan be NULL if the device was the phone itself (e.g. data from Apple Health or from the HealthMate app -> GPS)
ZDURATIONDuration of the measurement Duration is in seconds
ZTIMESTAMPStart Time of the measurementApple Cocoa Core Data Timestamp
In UTC
ZSTEPS# of steps recordedOnly filled for category 0 - Steps
ZDISTANCEDistance recordedOnly filled for category 0 - Steps
The distance is calculated by the app - based on the size of the person and the # of steps
ZCALORIESEARNEDAdditional calories earnedOnly filled for category 0 - Steps
This value is calculated by the app based e.g. on the size and the body composition of the user
ZHEARTRATE1Measured heart rateOnly filled for category 2 - Heart rate
ZLATITUDELatitude valueOnly filled for category 5 - Location
As decimal value
ZLONGITUDE

Longitude value

Only filled for category 5 - Location
As decimal value
ZALTITUDE

Altitude Value

Only filled for category 5 - Location
In meters
ZDIRECTION

Direction the device is going to

Only filled for category 5 - Location
In degree (0 - 359)
ZRADIUS

Radius around the coordinates - Means the uncertainty
Only filled for category 5 - Location
In meters
ZSPEED

Speed of the device

Only filled for category 5 - Location
In miles per hour (mph)
ZSPO2

Quality of the SPO2 value

Only filled for category 6 - SPO2
Value is in percent
ZASCENT1

Not known yetOnly filled for category 8 - Unknown
ZTEMPERATURE

Body temperature

Only filled for category 12 - Temperature
In degree Celsius


Tracking / Activities

The last thing I looked at were the activities tracked by my devices.
The activities are tracked in different cases:
  1. When I explicitly start tracking on one of my devices. e.g. when I go for a 5k run I use my watch and my phone connected to each other for tracking. - I can decide which type of activity is tracked in this case.
  2. When the device, mostly the watch in my case, recognize an activity - such as when I go by bike to my work place my watch recognizes this automatically. - What is detected works mostly fine on my side - but I had cases where something wrong was detected. So we need to keep this in mind when looking at the data. Sleeping is tracked the same way
Summary data of a day (24-hour timespan) is tracked (or better caluclated) for statistics and stored in the same database. Consist of e.g. total # of steps, spent time in heart rate zones, in/active minutes of the day.

Good to know: It is possible to delete tracked activities/sleep in the app. We will take a look at what this means for the data.

Path: %SandboxPath%/Library/Application Support/coredata
Name: [User_ID]_Tracks.sqlite

Tables of interest: ZTRACK
Overview of the interesting fields:

FieldMeaningAdditional info
ZDEVICEIDDevice ID of the source device

ZSUBCATEGORYID of the Subcategory - e.g.:
4 = Walking
16 = Others
18 = Running
22 = Cycling
The names of the subcategory can be joined from the table ZACTIVITYSUBCATEGORY from the same database
ZSTARTDATEStart time of the trackApple Cocoa Core Data Timestamp
In UTC
ZENDDATEEnd time of the trackApple Cocoa Core Data Timestamp
In UTC
ZREFERENCEDATEThe day the tracked data is referenced to.Apple Cocoa Core Data Timestamp
In UTC
ZMODIFIEDDATEDate of last modificationApple Cocoa Core Data Timestamp
In UTC
Also the app itself seems to modify anything - the timestamp is updated than. Almost all my Modified timestamps are younger than my created but I did not modify anything.
ZMANUALSTARTDATEStart time for manual tracking started e.g. on a watch Apple Cocoa Core Data Timestamp
In UTC
ZMANUALENDDATEEnd time for manual trackingApple Cocoa Core Data Timestamp
In UTC
ZSTEPSNumber of steps
ZCALORIESEARNED

Additional calories earned

ZISREMOVED

1 = Removed by user
0 = Default - Not removed

For my data I have entries that are set to 1 in the database. This means: Removed tracks are still in the database!
But these tracks are not shown in the UI anymore.
ZPAUSEDURATION

Pause in the tracking

In seconds
Only for manual tracking
ZNOTE

Added notes to the tracking by the user

ZTIMEZONE
Timezone where the device was in when tracking started.


ZCITY

Located city where the tracking was started

Only used when manual tracking is started

There a lot of other fields in there and additionally to this data one should take look into the table ZTRACKEXTENSION where specific data to the tracks can be stored.

Because of the amount of different types of data and fields stored in this database I decided to create SQL-queries for different result types

1. Tracked sleep
2. Tracked activities (manual and automatic)
3. Summary of the day


Tracked sleep

In the data to recognize sleep the following field values should be checked to identify the entries:

ZSUBCATEGORY IS NULL
ZTYPE = 36

My build SQL-Query with the relevant fields is than something like:
  SELECT  
  ZDEVICEID,
  DATETIME('2001-01-01', "ZSTARTDATE" || ' seconds') [STARTDATE],
  DATETIME('2001-01-01', "ZENDDATE" || ' seconds') [ENDDATE],
  DATETIME('2001-01-01', "ZREFERENCEDATE" || ' seconds') [REFERENCEDATE],
  DATETIME('2001-01-01', "ZMODIFIEDDATE" || ' seconds') [MODIFIEDDATE],
  DATETIME('2001-01-01', "ZMANUALSTARTDATE" || ' seconds') [MANUALSTARTDATE],
  DATETIME('2001-01-01', "ZMANUALENDDATE" || ' seconds') [MANUALENDDATE],
  ZLIGHTSLEEPDURATION,  
  ZREMSLEEPDURATION,  
  ZDEEPSLEEPDURATION,  
  ZDURATIONTOSLEEP,  
  ZTIMETOGETUP,  
  ZWAKEUPCOUNT,  
  ZWAKEUPDURATION  
  FROM ZTRACK  
  WHERE ZSUBCATEGORY IS NULL AND ZTYPE = 36

With this data you have the information, when and how the user slept.

Summary of the day

For the summary of a day the following fields should be checked to identify the entries:

ZTRACKID IS NULL
DEVICEID IS NULL

My build SQL-Query with the relevant fields is than something like:
   
  SELECT  
  DATETIME('2001-01-01', "ZSTARTDATE" || ' seconds') [STARTDATE],
  DATETIME('2001-01-01', "ZENDDATE" || ' seconds') [ENDDATE],
  DATETIME('2001-01-01', "ZREFERENCEDATE" || ' seconds') [REFERENCEDATE],
  DATETIME('2001-01-01', "ZMODIFIEDDATE" || ' seconds') [MODIFIEDDATE],
  DATETIME('2001-01-01', "ZMANUALSTARTDATE" || ' seconds') [MANUALSTARTDATE],
  DATETIME('2001-01-01', "ZMANUALENDDATE" || ' seconds') [MANUALENDDATE],
  ZDURATIONINACTIVE,  
  ZDURATIONINTENSE,  
  ZDURATIONMODERATE,  
  ZDURATIONSOFT,  
  ZSTEPS1,  
  ZDISTANCE1,  
  ZTIMEZONE
  FROM ZTRACK  
  WHERE ZTRACKID IS NULL AND ZDEVICEID IS NULL


With this you can get an overview how active the user was on the day - more details for a day can be get out of the measurements and the tracked activities.

Tracked Activities

The tracked activities are built together from three tables from the database

ZTRACK - for the main data
ZACTIVITYSUBCATEGORY - for the type of the activity
ZTRACKEXTENSION - for additional data to a track
And ZSTEPS never seems to be empty (in my data)


My build SQL-Query with the relevant fields is than something like:

  SELECT
  t.ZDEVICEID,
  sc.ZNAME,
  t.ZISREMOVED,
  t.ZPAUSEDURATION,
  DATETIME('2001-01-01', "ZSTARTDATE" || ' seconds') [STARTDATE],
  DATETIME('2001-01-01', "ZENDDATE" || ' seconds') [ENDDATE],
  DATETIME('2001-01-01', "ZREFERENCEDATE" || ' seconds') [REFERENCEDATE],
  DATETIME('2001-01-01', "ZMODIFIEDDATE" || ' seconds') [MODIFIEDDATE],
  DATETIME('2001-01-01', "ZMANUALSTARTDATE" || ' seconds') [MANUALSTARTDATE],
  DATETIME('2001-01-01', "ZMANUALENDDATE" || ' seconds') [MANUALENDDATE],
  te.ZINTENSEDURATION,  
  te.ZMODERATEDURATION,  
  te.ZLIGHTDURATION,  
  te.ZMIN,
  te.ZMAX,
  te.ZAVG,
  t.ZSTEPS,
  t.ZDISTANCE,  
  te.ZMINSPEED,  
  te.ZAVERAGESPEED,  
  te.ZMAXSPEED,  
  te.ZDISTANCE,  
  te.ZSTARTCOORDINATELATITUDE,  
  te.ZSTARTCOORDINATELONGITUDE,  
  te.ZENDCOORDINATELATITUDE,  
  te.ZENDCOORDINATELONGITUDE,  
  te.ZREGIONCENTERLATITUDE,  
  te.ZREGIONCENTERLONGITUDE,  
  te.ZMINTEMPERATURE,  
  te.ZAVGTEMPERATURE,  
  te.ZMAXTEMPERATURE,
  t.ZTIMEZONE
  FROM ZTRACK t
  INNER JOIN ZACTIVITYSUBCATEGORY sc ON t.ZSUBCATEGORY = sc.Z_PK
  INNER JOIN ZTRACKEXTENSION te ON t.Z_PK = te.ZTRACK
  WHERE t.ZSTEPS IS NOT NULL

There are quite a lot fields that are interesting. I recommend to filter for what you need.

Some things to know about the fields:

We have to fields with the label "ZDISTANCE".

The one from the table ZTRACK is the distance calculated without GPS.
The one from the table ZTRACKEXTENSION is measured via the GPS of the phone.
It depends on the tracked activity what type is used.


Conclusion and further work

Puh, lot of data in there. This was just a first view on the data and some extraction of them.
The data is stored a bit differently to how it is stored on Android - so I could not just copy and paste things ;-)

Analysis was fun. And I am not finished with the work yet.

Why? 
1. Well some of the data I looked for, I could not find - 
For example:
- The detailed GPS data for a track - not just the start, center and end coordinates

2. After I did the acquisition of the data I connected Withings with Apple Health - I want to take a look what effect this has on the data - and also how does the Apple health data looks than.

3. My SQL-Queries are not fully finalized. I want to make them more versatile.

4. Some thoughts on visualization - at the moment just tables - but diagrams and maps would be fine.


I scripted different parsers for the Withings Health Mate App artifacts to be included in iLEAPP. I think it is important to give back and it is also a nice training for me.


Health Apps always store very interesting data. Data that can help to determine what a user did at a specific time. But, just like always - never look only on the data of one app or one type. Always try to correlate data and see if different data (types) lead to the same result.

Please - if you use my queries - double check everything. They should help to get a first view on the data. But look for yourself in the database to verify. Especially the query for the Tracked activities was tricky.


I am just happy that I had a bit of time to look into the app also on iOS :-)

Hope you had a nice read.