It's occurred to me, it's stupid to try and reverse engineer this when there's an expert source available: Gadgetbridge is OSS after all.
It looks like the value of RAW_KIND is checked here
for (final HuamiExtendedActivitySample sample : samples) {
if (sample.getRawKind() == TYPE_SLEEP) {
// Band reports type sleep regardless of sleep type, so we map it to custom raw types
// These thresholds are arbitrary, but seem to somewhat match the data that's displayed on the band
sample.setDeepSleep(sample.getDeepSleep() & 127);
sample.setRemSleep(sample.getRemSleep() & 127);
if (sample.getRemSleep() > 55) {
sample.setRawKind(TYPE_CUSTOM_REM_SLEEP);
sample.setRawIntensity(sample.getRemSleep());
} else if (sample.getDeepSleep() > 42) {
sample.setRawKind(TYPE_CUSTOM_DEEP_SLEEP);
sample.setRawIntensity(sample.getDeepSleep());
} else {
sample.setRawIntensity(sample.getSleep());
}
}
}
}
TYPE_SLEEP is defined further up
public static final int TYPE_SLEEP = 120;
public static final int TYPE_CUSTOM_DEEP_SLEEP = TYPE_SLEEP + 1;
public static final int TYPE_CUSTOM_REM_SLEEP = TYPE_SLEEP + 2;
Although the code checks for 120, it looks like it overrides the type when writing into it's own database (so we can use RAW_KIND to identify the sleep type without having to reimplement the same logic)
So, we want rows where RAW_KIND is between 120 and 122 (inclusive)
sqlite> select * FROM MI_BAND_ACTIVITY_SAMPLE WHERE TIMESTAMP > 1693094700 AND TIMESTAMP < 1693101900 and RAW_KIND BETWEEN 120 AND 122;
TIMESTAMP|DEVICE_ID|USER_ID|RAW_INTENSITY|STEPS|RAW_KIND|HEART_RATE
1693101540|1|1|0|0|122|60
1693101660|1|1|0|0|121|61
1693101720|1|1|0|0|122|61
1693101780|1|1|0|0|122|63
1693101840|1|1|0|0|122|63
That's a lot fewer than I was expecting - should look at what it's doing with intensity then.
I need to step away from this for a while, but it looks like the answer can probably be found here: That's where GB calculates sleep stats for display in the charts - makes more sense to interpret from there (the output side) than the Huami specific stuff (the input side).
The other thing we need to do, is work out what RAW_INTENSITY denotes in this context:
sqlite> select TIMESTAMP, RAW_INTENSITY, RAW_KIND FROM MI_BAND_ACTIVITY_SAMPLE WHERE TIMESTAMP > 1693094700 AND TIMESTAMP and RAW_KIND BETWEEN 120 AND 122 AND RAW_INTENSITY <> 0;
TIMESTAMP|RAW_INTENSITY|RAW_KIND
1693108440|6|121
1693109640|7|121
1693118220|10|121
There's a bitwise AND performed on the value, and then under some circumstances the value is overridden
if (sample.getRawKind() == TYPE_SLEEP) {
// Band reports type sleep regardless of sleep type, so we map it to custom raw types
// These thresholds are arbitrary, but seem to somewhat match the data that's displayed on the band
sample.setDeepSleep(sample.getDeepSleep() & 127);
sample.setRemSleep(sample.getRemSleep() & 127);
if (sample.getRemSleep() > 55) {
sample.setRawKind(TYPE_CUSTOM_REM_SLEEP);
sample.setRawIntensity(sample.getRemSleep());
} else if (sample.getDeepSleep() > 42) {
sample.setRawKind(TYPE_CUSTOM_DEEP_SLEEP);
sample.setRawIntensity(sample.getDeepSleep());
} else {
sample.setRawIntensity(sample.getSleep());
}
}
}
The bit I can't quite get my head around - the RAW_KIND is 121 in those results, so it should be a deep sleep. I don't see deep sleep in GB's graphs at that time.
Ahhh, that'd be because it only lasted about 5 mins
sqlite> select TIMESTAMP, RAW_INTENSITY, RAW_KIND FROM MI_BAND_ACTIVITY_SAMPLE WHERE TIMESTAMP > 1693108438 AND TIMESTAMP < 1693109640 and RAW_KIND BETWEEN 120 AND 122 ;
TIMESTAMP|RAW_INTENSITY|RAW_KIND
1693108440|6|121
1693108740|0|122
1693109400|0|122
1693109460|0|122
1693109520|0|122
1693109580|0|122
OK, so lets start simple - pull out the sleep associated entries and insert them into a field indicating sleep + level, will see what that looks like when charted out
It's not very graph friendly, but this seems to work
data_query = ("SELECT TIMESTAMP, DEVICE_ID, RAW_INTENSITY, RAW_KIND"
" FROM MI_BAND_ACTIVITY_SAMPLE "
f"WHERE TIMESTAMP >= {query_start_bound} "
"AND RAW_KIND BETWEEN 120 AND 122 "
"ORDER BY TIMESTAMP ASC")
res = cur.execute(data_query)
for r in res.fetchall():
if r[3] == 120:
sleep_type = "light"
elif r[3] == 121:
sleep_type = "deep"
elif r[3] == 122:
sleep_type = "REM"
row_ts = r[0] * 1000000000
row = {
"timestamp": row_ts, # Convert to nanos
"fields" : {
"intensity" : r[2],
f"{sleep_type}_sleep" : 1
},
"tags" : {
"device" : devices[f"dev-{r[1]}"],
"sample_type" : "sleep"
}
}
results.append(row)
I get increases about where the Gadgetbridge graph shows them (it's also just occurred to me that GB skips anything with a duration < 5 mins, which is why I couldn't match that other deep sleep)
So, the question now becomes how best to write it out to InfluxDB.
I think the answer is probably to write out 1 minute intervals (like we've just done in #13 for stress).
That way it's relatively easy to generate a graph showing state, as well as to calculate proportion of time spent in that state.
As noted above, Gadgetbridge seems to look backwards rather than forwards (i.e. that deep sleep marker marks the end of a period of deep sleep, not the start). So, we probably want to work backwards through the records
But, to do that, we also need to figure out how it records that I've woken up (so that we don't report that the whole day was spent asleep)
With that in mind then, to work out time spent asleep we want to:
Query for RAW kind 120, 121, 122 or 112
Order by TIMESTAMP
If the next point has raw_kind 112 don't insert sleeps
This does that
SELECT
TIMESTAMP,
DEVICE_ID,
RAW_INTENSITY,
RAW_KIND,
LEAD (TIMESTAMP, 1) OVER (PARTITION BY DEVICE_ID, USER_ID ORDER BY TIMESTAMP) NEXT_TS,
LEAD (RAW_KIND, 1) OVER (PARTITION BY DEVICE_ID, USER_ID ORDER BY TIMESTAMP) NEXT_KIND
FROM MI_BAND_ACTIVITY_SAMPLE
WHERE
((RAW_KIND=112 AND RAW_INTENSITY=1)
OR RAW_KIND BETWEEN 120 AND 122)
ORDER BY TIMESTAMP;
But, there's still something we're missing.
If we look at the entries around that 23:02 timestamp:
There's just no way that I was awake from 23:02 -> 02:59. Gadgetbridge's report says I fell asleep at 01:02 (which still feels too late, but more reasonable).
There must be another entry that we're not accounting for here
sqlite> select * FROM MI_BAND_ACTIVITY_SAMPLE WHERE TIMESTAMP >= 1693094460 AND TIMESTAMP <= 1693094580;
TIMESTAMP|DEVICE_ID|USER_ID|RAW_INTENSITY|STEPS|RAW_KIND|HEART_RATE
1693094460|1|1|15|0|240|82
1693094520|1|1|10|0|249|86
1693094580|1|1|0|0|240|81
The middle timestamp is 01:02, suggesting that RAW_KIND 249 might indicate falling asleep (although, if so, what's that intensity?)
SELECT
TIMESTAMP,
DEVICE_ID,
RAW_INTENSITY,
RAW_KIND,
LEAD (TIMESTAMP, 1) OVER (PARTITION BY DEVICE_ID, USER_ID ORDER BY TIMESTAMP) NEXT_TS,
LEAD (RAW_KIND, 1) OVER (PARTITION BY DEVICE_ID, USER_ID ORDER BY TIMESTAMP) NEXT_KIND
FROM MI_BAND_ACTIVITY_SAMPLE
WHERE
((RAW_KIND=112 AND RAW_INTENSITY=1)
OR RAW_KIND BETWEEN 120 AND 122
OR RAW_KIND=249
)
ORDER BY TIMESTAMP;
I'm not convinced this is right, but needs graphing out to be sure
It's possible to graph out, but I'm not sure that I believe the values
I don't want to rip the code back out, but I also don't want these questionable figures to be taken as read, so I'm going to add an experimental flag and hide this behind it.
Activity
27-Aug-23 14:06
assigned to @btasker
27-Aug-23 14:10
The tables being written into are
You'd think it'd be
MI_BAND_ACTIVITY_SAMPLE
but it doesn't have any sleep related columns.Oh.... UNLESS... what if we need to be looking at a combination of
RAW_KIND
andINTENSITY
?27-Aug-23 14:16
The GB interface says I slept from 01:02 to 08:59 so generating some timestamps in that period
Pulling a sample out
What's interesting is there are periods where
RAW_INTENSITY
changeThere are also periods where
RAW_KIND
changes though27-Aug-23 14:33
It's occurred to me, it's stupid to try and reverse engineer this when there's an expert source available: Gadgetbridge is OSS after all.
It looks like the value of
RAW_KIND
is checked hereTYPE_SLEEP
is defined further upAlthough the code checks for 120, it looks like it overrides the type when writing into it's own database (so we can use
RAW_KIND
to identify the sleep type without having to reimplement the same logic)So, we want rows where
RAW_KIND
is between 120 and 122 (inclusive)That's a lot fewer than I was expecting - should look at what it's doing with intensity then.
I need to step away from this for a while, but it looks like the answer can probably be found here: That's where GB calculates sleep stats for display in the charts - makes more sense to interpret from there (the output side) than the Huami specific stuff (the input side).
27-Aug-23 14:37
Wait... I'm an idiot.
of course that query only returned a few samples - it was specifically filtered to run between a couple of timestamps.
Returns many more. Go figure.
It looks like the GB code works backwards, so if we want duration, we need to use
LAG()
to grab the last sleep timestamp.I'll think about how I want to represent the sleep data once I get back, I was never quite happy with the way we did it in
zepp_to_influxdb
27-Aug-23 15:27
The other thing we need to do, is work out what
RAW_INTENSITY
denotes in this context:There's a bitwise AND performed on the value, and then under some circumstances the value is overridden
The bit I can't quite get my head around - the
RAW_KIND
is121
in those results, so it should be a deep sleep. I don't see deep sleep in GB's graphs at that time.Ahhh, that'd be because it only lasted about 5 mins
OK, so lets start simple - pull out the sleep associated entries and insert them into a field indicating sleep + level, will see what that looks like when charted out
27-Aug-23 15:38
It's not very graph friendly, but this seems to work
I get increases about where the Gadgetbridge graph shows them (it's also just occurred to me that GB skips anything with a duration < 5 mins, which is why I couldn't match that other deep sleep)
27-Aug-23 16:52
So, the question now becomes how best to write it out to InfluxDB.
I think the answer is probably to write out 1 minute intervals (like we've just done in #13 for stress).
That way it's relatively easy to generate a graph showing state, as well as to calculate proportion of time spent in that state.
As noted above, Gadgetbridge seems to look backwards rather than forwards (i.e. that deep sleep marker marks the end of a period of deep sleep, not the start). So, we probably want to work backwards through the records
But, to do that, we also need to figure out how it records that I've woken up (so that we don't report that the whole day was spent asleep)
27-Aug-23 16:52
mentioned in commit 21a3ed95b2fabb731b3cf48f90ab07173f692754
Message
Start looking at detecting sleep. utilities/gadgetbridge_to_influxdb#14
This is disabled for now as still need to work out how we want to present the values
27-Aug-23 22:17
OK, so it looks like the watch will log me waking up with
RAW_KIND
112 andRAW_INTENSITY
1I found it by checking waking times in Gadgetbridge and then looking for entries around that time.
27-Aug-23 22:31
With that in mind then, to work out time spent asleep we want to:
TIMESTAMP
This does that
But, there's still something we're missing.
If we look at the entries around that 23:02 timestamp:
There's just no way that I was awake from 23:02 -> 02:59. Gadgetbridge's report says I fell asleep at 01:02 (which still feels too late, but more reasonable).
There must be another entry that we're not accounting for here
27-Aug-23 22:41
Looking around the 01:02 mark:
The middle timestamp is 01:02, suggesting that
RAW_KIND
249 might indicate falling asleep (although, if so, what's that intensity?)I'm not convinced this is right, but needs graphing out to be sure
27-Aug-23 23:13
mentioned in commit 09418d6e183cf1bd86f1c028def86b8b5a3deb40
Message
Capture and write sleep data (utilities/gadgetbridge_to_influxdb#14)
27-Aug-23 23:16
I think, of all the things we've added, this one definitely needs playing with in Grafana before we consider this done.
27-Aug-23 23:16
mentioned in commit df03fa00ca30c5b33a18d451f55616ea73f1b915
Message
Add textual representation of sleep state (utilities/gadgetbridge_to_influxdb#14)
This should make it easier to use Grafana's state-change chart type
28-Aug-23 13:27
It's possible to graph out, but I'm not sure that I believe the values
I don't want to rip the code back out, but I also don't want these questionable figures to be taken as read, so I'm going to add an experimental flag and hide this behind it.
28-Aug-23 13:33
mentioned in commit bb134a1bb31025c553382ec293d81dd37ba5a367
Message
Add support for new
EXPERIMENTAL_OPTS
env var and mark sleep as experimental (utilities/gadgetbridge_to_influxdb#14)The new variable should be specified as a comma seperated list.
If
SLEEP
is not in it, sleep data will not be collected.As noted in the GL ticket, this is because the data being collected doesn't currently seem plausible.
29-Aug-23 13:13
changed title from Sleep {-d-}ata {-isn't being written to InfluxDB-} to Sleep {+D+}ata {+Support+}
29-Aug-23 13:15
I don't want this to block the release that we're (otherwise) storming towards, so I've updated the issue title and marked as experimental.
Will raise a follow-up issue once I've time to revisit this