手腕上的 Apple Watch 每天告诉我“站立 10 小时”,Mac 的屏幕使用时间又说我今天用了 7 个多小时。两个数字放在一起就很奇怪:一天醒着十五六个小时,站了 10 个小时,又在电脑前坐了 7 个小时,加起来已经快满了,那吃饭、通勤、开会都去哪了?
我真正想知道的是这么几件事:
- 一天里到底站了多久、坐了多久?
- 在屏幕前工作了多久?不同时间段里,工作状态怎么样?
- 当时周围的环境怎么样,比如有多吵?
- 手上只有 iPhone、Apple Watch 和 MacBook,能不能把这一天完整还原出来?有没有现成的软件已经做到了?
本文就顺着这几个问题,从每台设备“到底记下了什么”开始,一层一层往下拼。
一、站立与久坐:手表到底测了什么
现象:站立 10 小时,不等于站了 10 小时
先看那个“站立 10 小时”。它来自活动圆环的第三环,对应 HealthKit 里的 appleStandHour。Apple 的定义是:这一小时里站起来并活动了至少 1 分钟,就算一个站立小时1。所以“站立 10 小时”的真实含义是“有 10 个小时,每个小时里你至少站过 1 分钟”。哪怕每小时只站 1 分钟,也能拿满。
这个指标的设计目的是提醒人别一直坐着,而不是计量站了多久。真正计量时长的是另一个类型 appleStandTime:iOS 13 / watchOS 6 起提供,单位是时间,样本是累计型的,加起来就是当天站立的分钟数2。健康 App 里“站立分钟”看到的就是它。
那坐着呢?HealthKit 里根本没有“坐着”这个类型。 这不是遗漏,而是手表测不出来:它戴在手腕上,坐着和站着不动时,手腕的加速度几乎一样。
第一次尝试:坐着 = 醒着 − 站着 − 走路
最直觉的办法是做减法:把醒着的时间减去站立分钟、减去步行和运动,剩下的就是坐着。睡眠时间手表也有记录,醒着的区间不难拿到。
这个办法有三个漏洞:
- 没戴表的时间会被算成坐着。 关掉手腕检测,或者表摘下来充电,手表就不记站立,也没有后台心率3。减法会把这段空白算进“坐”。
- 站着不动可能不算站。 Apple 的定义是“站起来并活动”,在白板前站着讲半小时、排队一动不动,手表未必记满(这一点是根据定义的推断,Apple 没有公开判定算法)。
- 坐着时手臂动得多,可能被算成站。 社区里常有人反映坐着打字、开车也会“站立达标”,说明判断依据主要是手腕的运动(同样是推断)。
那换一个更底层的数据呢?CoreMotion 的 CMMotionActivity 会把运动状态分成静止、步行、跑步、乘车、骑行和未知几类,各带一个置信度4。可坐和站在这里都是 stationary,还是分不出来,而且系统只保留最近 7 天5。再往下一层,watchOS 的 CMSensorRecorder 可以让手表在后台以 50 Hz 录原始加速度,单次最多 12 小时,数据保留 3 天6。有了原始数据,理论上可以自己训练一个坐站分类器,但手腕上的加速度本来就缺少区分坐站的信息(推断:研究里区分坐站通常要把传感器贴在大腿上,靠大腿是水平还是竖直来判断)。
解决:找一个“坐着”的旁证
既然手表测不出坐着,就找别的设备作证。Mac 恰好是一个很好的“坐着”的证据:键盘和鼠标有输入,人几乎一定在桌前;手表又没记到站立,那这段时间大概率是坐着的。图 1 下半部分就是这样推出来的:10:00–10:52 Mac 一直有输入,扣掉 10:07–10:09 那 2 分钟站立,坐了 50 分钟。
这个规则有一个明显的例外:用升降桌站着办公。这时候 Mac 有输入、手表也可能记到站立,两份证据互相矛盾。ActivityWatch 社区有一个 aw-watcher-table 插件直接读升降桌的高度7,是目前能精确区分坐站的少数办法之一。
而 10:52 之后那 8 分钟,人离开了 Mac,手表也没记到站立,就真的没有任何证据了:可能是去会议室坐着开会,也可能站在茶水间聊天。
所以“坐着”这件事,最后要靠 Mac 来补。那 Mac 自己能交出哪些记录?
二、屏幕前时间:Mac 能交出什么
现象:屏幕使用时间看得到,拿不出来
Mac 自带的“屏幕使用时间”已经在记录每个 App 的使用时长,问题是它不给第三方用。
Apple 为屏幕使用时间提供的公开接口是 DeviceActivity 一族框架,但它的设计很明确:报表由 App 的一个扩展来渲染,扩展跑在沙箱里,不能联网,也不能把数据带出扩展的地址空间8。也就是说,你可以在自己的 App 里“画”一张屏幕时间报表给用户看,却拿不到报表背后的原始记录。
第一次尝试:直接读系统数据库
数据总得存在某个地方。取证领域早就研究过:macOS 和 iOS 把应用使用记录写在 ~/Library/Application Support/Knowledge/knowledgeC.db 里,按“流”分类存放:比如 /app/usage 记录前台应用,/display/isBacklit 记录屏幕亮灭,时间精确到秒9。较新的系统又把这类记录迁到了 Biome,变成一种叫 SEGB 的私有二进制格式10。
Timing 这类商业软件就是走这条路的:它导入屏幕使用时间,需要用户给它“完全磁盘访问权限”,官方文档也直说 Apple 改了存储方式,这个功能可能就会失效11。同一页还提到系统界面只给到小时粒度、只保留四周。
所以读系统数据库能用,但有两个问题:格式私有、没有文档,每次系统升级都可能变;而且它只记录“哪个 App 在前台”,前台开着不等于人在看。
解决:自己记录“人在不在键盘前”
更稳的办法是在 Mac 上跑一个自己的记录程序,只回答两个问题:
- 人在不在? CoreGraphics 的
secondsSinceLastEventType能直接拿到距离上一次键鼠输入过了多少秒12,超过一个阈值就当人离开了。 - 在用什么? 监听前台应用切换,就能知道是哪个 App;想要窗口标题,还得给它“辅助功能”权限13。
这正是开源工具 ActivityWatch 的做法。它的 aw-watcher-afk 判断离开:超过 3 分钟没有键鼠输入,就从最后一次输入的时刻起记为 afk;aw-watcher-window 记录前台应用和窗口标题13。两路记录都存在本机,用 localhost:5600 上的 REST 接口就能取出来14。
图 2 也暴露了这种做法的边界:它测的是“手在键盘上”,不是“眼睛在屏幕上”。读长文档、看视频、开视频会议时手不动,就会被记成离开。ActivityWatch 的缓解办法是浏览器标签页在播放声音时不判离开13;把阈值从 3 分钟调到 5 分钟也能少漏一些,代价是真离开时会多算几分钟。
到这里,“在屏幕前多久”有了一个分钟级的答案。但用户还想知道不同时间段里工作状态怎么样,这就不只是“在不在”的问题了。
三、工作状态:从行为和身体两侧看
什么叫“工作状态”
“状态好不好”没有一个能直接测量的物理量。我们能做的,是找几个可以测量、又和状态有关的信号,从两个方向去逼近:人在电脑上做了什么(行为),以及身体当时处在什么状态(生理)。
| 信号 | 来源 | 能说明 | 说明不了 |
|---|---|---|---|
| 行为(Mac) | |||
| 连续停留在一个应用 | aw-watcher-window | 长时间不切换,往往是在专注做一件事 | 在 IDE 里发呆也算“专注” |
| 应用切换频率 | aw-watcher-window | 一刻钟切换几十次,大概率在被打断 | 查资料时频繁切换也可能很高效 |
| 键鼠输入量 | aw-watcher-input | 在产出还是在浏览 | 思考时手是不动的 |
| 屏幕内容 | 截屏 + 大模型 | 具体在干什么,语义最丰富 | 隐私代价最高 |
| 生理(Apple Watch) | |||
| 心率 | HeartRate | 紧张、活动、咖啡因都会让它升高 | 分不清是哪个原因 |
| HRV | HRV SDNN | 自主神经的负荷 | 白天样本太稀,只能看大趋势 |
行为信号是分钟级、连续的;生理信号在白天是稀疏的点。两侧单独看都会误判,叠在一起才有参考价值。
行为一侧:切换和停留
行为信号里最实用的是应用切换。图 2 那半小时里切换了 4 次,主要停在 Xcode;如果另一个半小时在 Slack、邮件、浏览器之间切换了 40 次,不用任何算法也看得出来,后者是一段被打断的时间。RescueTime、Rize 这类工具给出的“专注度评分”,核心也是这套逻辑:按应用分类打分,再看连续时长和切换1516。
它的局限也写在表里:应用名只说明“在哪”,不说明“在干什么”。同样是开着浏览器,可能在查文档,也可能在刷新闻。于是有了一类新工具:定时截屏,再让大模型读截图,总结这段时间在做什么。开源的 Dayflow 就是这样生成一天的活动卡片时间线的,模型可以跑在本地17;screenpipe 则持续录屏、做 OCR 和语音转写18。语义最丰富,隐私代价也最高。
身体一侧:心率能用,HRV 太稀
Apple Watch 白天会在后台测心率。按 Apple 支持页的说法,较新的机型白天可以持续测量,老机型则在你静止时周期性地测、间隔不固定19。总之,心率是一条足够密的曲线,能对上每一段工作。
HRV 就不一样了。我在从两个对不上的 HRV 数字说起里讲过,手表只在静止、戴稳时才测 HRV,白天的样本很稀,单次读数的噪声又大。拿它去判断“10:00–10:15 是不是专注”是不现实的,它只适合回答“今天下午整体是不是比上午紧”这种大尺度的问题。
而且生理信号不特异:心率升高可能是紧张,也可能是刚爬完楼梯、刚喝了咖啡。所以它要和行为信号放在一起看:同样是心率 90,一段是 Mac 上连续 40 分钟写代码,一段是刚从楼下走上来,含义完全不同。
到这里,人这一侧的信号差不多齐了。还剩最后一块拼图:人所处的环境。
四、环境:噪声、日光与地点
噪声:手表一直在听,但不录音
Apple Watch 的噪声 App 用麦克风测量环境声级,写进 HealthKit 的 environmentalAudioExposure,单位是 A 计权声压级(dBA)20。按 Apple 支持页的说法,它在一天里周期性地测量,只测声级,不录音也不保存任何声音;具体多久测一次,Apple 没有公开21。所以我们能拿到的是一串带时间区间的分贝值,而不是音频。
这里有一个容易踩的坑:分贝不能直接平均。 分贝是对数刻度,65 dB 的声能是 55 dB 的 10 倍。一刻钟里 5 分钟是 54 dB、10 分钟是 66 dB,算术平均是 62 dB,但按声能平均要先换回能量:
其中 是每段的时长, 是这段的分贝值, 是总时长。代入上面的例子:
结果是 64.4 dB。这就是“等效连续声级” ,HealthKit 里每个噪声样本本身就是这种值。差的这 2 dB,对应的声能差了将近 60%。
另外两个相关的类型也值得记下:headphoneAudioExposure 记录耳机里的音量;environmentalSoundReduction(iOS 16 起)记录戴降噪耳机时环境声被压低了多少22。后者很有意思:一段时间里一直在降噪,往往说明环境吵、人又想专注(推断)。
日光与地点
- 日光时间:watchOS 10 起,较新的 Apple Watch 用环境光传感器估算你在日光下待了多久,写进
timeInDaylight23。它可以粗略地区分室内和户外。 - 地点:Core Location 的
CLVisit会在你到达、离开一个地点时通知 App,给出坐标和起止时间24。“办公室 09:05–12:10”“餐厅 12:20–13:05”这类停留就是这样来的。iPhone 自己的“重要地点”则没有公开接口,第三方读不到。
拿不到的那部分:SensorKit
Apple 其实有一个更全的传感器框架 SensorKit:环境光、气压、键盘使用指标、手机使用报告、常去地点、是否戴在手腕上、语音指标……几乎就是“还原一天”的现成原料25。但它只授予获得 Apple 批准的研究项目26,新数据还要等 24 小时才能读取27。个人开发者和普通 App 都拿不到。
现在,人、身体、环境三方面的数据都找到了来源。接下来的问题是:它们怎么拼到同一条时间线上?
五、拼成一条时间线
问题:每路数据的“时间形状”不一样
把这些数据摆在一起,会发现它们记录时间的方式各不相同:
- 区间累计:站立分钟、步数。一条样本说的是“10:07–10:12 之间站了 5 分钟”“09:58–10:03 走了 200 步”。
- 时间点:心率。一条样本就是“10:07 心率 78”。
- 区间均值:噪声。一条样本是“10:05–10:20 的等效声级 66 dB”。
- 事件区间:Mac 的 afk 和前台窗口。一条事件是“09:00–09:12 not-afk”。
数据也分在两台机器上:健康数据从手表同步到 iPhone 的 HealthKit;Mac 的记录在本机的 ActivityWatch 里。
健康数据怎么搬到 Mac 上?最省事的是健康 App 里“导出所有健康数据”,得到一个 export.xml,每条样本是一个 <Record> 节点,带类型、数值、单位和起止时间。缺点是要手动点、文件动辄几百 MB 甚至几个 GB。想自动化,可以用 Health Auto Export 这类 App 定时把指定类型推到本地或自建的接口28。
第一次尝试:按事件对齐
最直觉的做法是以 Mac 的事件为主轴:每一段 not-afk 事件,去找它时间范围内的心率、噪声、站立。问题是 Mac 事件的长短差很多,有的 10 秒,有的两小时;人离开 Mac 的时间段又根本没有事件,手表那边的数据就挂不上去。
解决:切成等长的格子,按重叠比例分摊
更稳的办法是先把一天切成等长的格子(比如 15 分钟一格),再把每路数据按它的时间形状摊进格子里:
- 区间累计型按重叠时长的比例分摊。09:58–10:03 的 200 步,有 3 分钟落在 10:00–10:15 这一格,就分给它 120 步。
- 时间点型取格子内的平均。
- 区间均值型按重叠时长加权,噪声还要按上一节的声能平均。
- 事件区间型直接算重叠了多少秒。
每一格都填好之后,再用几条简单的规则给它贴一个状态标签:
- 步数很多:走路或通勤。
- Mac 有输入超过 10 分钟:在屏幕前;手表没记到站立就是坐着,记到了就是“站过”。
- Mac 没输入、手表记到站立:站着或走动。
- Mac 没输入、手表也没记到站立、但有心率:静止,坐站不明。
- 连心率都没有:多半是没戴表。
最后两条很关键:规则不知道的时候,要老实写“不知道”,而不是把它塞进“坐着”。图 1 里那 8 分钟就属于这一类。
把一整天的格子加起来,就得到了开头想要的几个数:站了多久、坐在屏幕前多久、还有多少时间是“静止但不知道坐还是站”。文末给出了完整的脚本。
这件事自己拼要写不少代码,那有没有软件已经做好了?
六、现有软件做到了哪一步
按前面拆出的几个维度,把常见的软件放在一起比一比:
| 软件 | 站 / 坐 | 屏幕前 | 工作状态 | 噪声 | 地点 |
|---|---|---|---|---|---|
| Apple 自带 | |||||
| 健康 / 健身 | 站立小时、站立分钟 | 无 | 心率、HRV 原始数据 | 有数据 | 无 |
| 屏幕使用时间 | 无 | 小时粒度,不能导出 | 无 | 无 | 无 |
| Mac 时间追踪 | |||||
| ActivityWatch | 仅升降桌插件 | 分钟级,本地 | 窗口、键鼠量 | 无 | 无 |
| Timing | 无 | 分钟级,含手机 | 项目归类、AI 摘要 | 无 | 无 |
| RescueTime / Rize | 无 | 有,云端 | 专注度评分 | 无 | 无 |
| Dayflow | 无 | 截屏推断 | 大模型总结 | 无 | 无 |
| 手机与聚合 | |||||
| Arc Timeline | 步行等出行方式 | 无 | 每段停留的心率 | 无 | 停留与行程 |
| Health Auto Export | 导出站立分钟 | 无 | 导出心率、HRV | 导出分贝 | 无 |
| Exist | 按天 | 接 RescueTime | 按天找相关性 | 无 | 按天 |
没有一行是全绿的。每个软件只管一两块,且没有一个把 Mac 上的工作记录和手表上的身体数据放在同一条分钟级时间线上。
几个值得单独说的:
- Mac 端,ActivityWatch 开源、数据在本地、有 REST 接口,最适合拿来自己拼7。Timing 更省心,还能把 iPhone 的屏幕使用时间导进来,但如上一节所说,它依赖系统私有数据库11。
- 地点,Arc Timeline 用 iPhone 的定位和运动传感器把一天切成“停留”和“行程”,还会在每段旁边读 Apple Health 的步数和心率,数据存在手机本地,可以每天自动导出 JSON29。它是线下这一半做得最完整的。
- 健康数据搬运,Health Auto Export 可以把 HealthKit 里一百多种指标按分钟导出为 JSON/CSV,或推到自己的服务28。前面图 4 里手动导出
export.xml那一步,可以用它替代。 - 聚合,Exist 能把 Apple Health 和 RescueTime 等数据放在一起,但粒度是按天,做的是“哪些因素和心情相关”,不是时间线30。
- 个人实践,Felix Krause 的 howisFelix.today 从 2019 年起记录了约 38 万条个人数据,来源是 RescueTime、地点签到、天气接口和一个 Telegram 机器人手动录入,统一存进自己的数据库3132。它说明这件事做得到,也说明了代价:相当一部分靠手动录入,粒度也主要是按天。
- 已经消失的路线,Rewind(后改名 Limitless)曾主打 Mac 录屏加随身录音的“全量回放”,被 Meta 收购后已停止录屏和录音33。
所以结论是:目前没有一个软件能单独做到“还原一天”。 最接近的组合是 Mac 上的 ActivityWatch 或 Timing、手机上的 Arc Timeline,加上 Health Auto Export 把手表数据搬出来,最后自己按时间对齐。空白主要在两处:一是坐和站的精确区分,二是把 Mac 上的工作和手表上的身体数据放在同一条分钟级时间线上。
七、边界:哪些还原不了
拼到这一步,还有几块是现有设备补不上的:
- 离开电脑后的“坐还是站”:开会、吃饭、在沙发上看手机,手表和 Mac 都无法区分。可以借助地点(在会议室,多半坐着)或日历(这段时间有会)来猜,但仍然是猜。
- 不动手的工作:读文档、思考、开会,在键鼠记录里都表现为“离开”。截屏加大模型能补上一部分,代价是隐私。
- 私有数据会变:屏幕使用时间的数据库、Biome 的格式都没有文档,系统升级就可能失效;SensorKit 这样的官方通道又不对个人开放。
- 采样不均匀:心率密、HRV 稀、噪声的测量间隔不公开。格子切得越细,空格越多;切得越粗,越看不出一段时间内部的变化。15 分钟是一个折中。
小结
从“站立 10 小时 + 屏幕 7 小时”这两个对不上的数字出发,我们一路拼到了:
- 手表的“站立小时”不是站立时长:它只要求每小时站过 1 分钟;真正的时长在
appleStandTime里。HealthKit 没有“坐着”,手腕也很难区分坐站。 - “坐着”要靠 Mac 作证:Mac 有键鼠输入、手表没记到站立,才推断为坐着;两边都没有证据的时间,要老实标成“不明”。
- 屏幕前时间要自己记:系统的屏幕使用时间拿不出原始数据,ActivityWatch 这类工具用“前台窗口 ∩ 不在 afk”给出分钟级记录,代价是漏掉不动手的时间。
- 工作状态要两侧一起看:应用切换和停留说明人在做什么,心率说明身体在承受什么;HRV 太稀,只能看大趋势。
- 环境数据手表已经在采:噪声以等效声级存进 HealthKit,平均时要按声能平均;日光时间和
CLVisit补上室内外和地点。 - 拼接的关键是等长格子:四种时间形状的数据按重叠比例摊进 15 分钟一格,再用规则贴标签。
- 没有现成软件全覆盖,最接近的是 ActivityWatch / Timing + Arc Timeline + Health Auto Export 的组合。
下面是图 4、图 5 那条路的完整实现:读健康 App 导出的 export.xml 和本机 ActivityWatch 的记录,输出 15 分钟一格的时间表和全天合计。只用 Python 标准库。
"""把 Apple 健康导出(export.xml)和 ActivityWatch 的记录拼成一张 15 分钟一格的时间表。
用法:python3 day_timeline.py export.xml 2026-11-01依赖:只用标准库;ActivityWatch 需在本机 5600 端口运行。"""import jsonimport mathimport sysimport urllib.parseimport urllib.requestimport xml.etree.ElementTree as ETfrom collections import defaultdictfrom datetime import datetime, timedelta
EXPORT, DAY = sys.argv[1], sys.argv[2]TZ = datetime.now().astimezone().tzinfoSTART = datetime.fromisoformat(DAY).replace(hour=7, tzinfo=TZ)END = START + timedelta(hours=17) # 07:00 – 24:00SLOT = timedelta(minutes=15)SLOTS = [START + i * SLOT for i in range(int((END - START) / SLOT))]
WANTED = { "HKQuantityTypeIdentifierAppleStandTime": "stand", # 分钟 "HKQuantityTypeIdentifierStepCount": "steps", "HKQuantityTypeIdentifierHeartRate": "hr", # 次/分 "HKQuantityTypeIdentifierEnvironmentalAudioExposure": "noise", # dBA}
def overlap(a0, a1, b0, b1): """两段时间重叠的秒数。""" return max(0.0, (min(a1, b1) - max(a0, b0)).total_seconds())
def spread(table, key, t0, t1, value, mode): """把一条样本分摊到它覆盖的格子里。sum 按重叠比例摊,mean 记成带权样本。""" span = max((t1 - t0).total_seconds(), 1.0) for s in SLOTS: sec = overlap(s, s + SLOT, t0, max(t1, t0 + timedelta(seconds=1))) if sec: if mode == "sum": table[s][key] += value * sec / span else: table[s].setdefault(key + "_w", []).append((value, sec))
# 1. 健康数据:流式解析,导出文件动辄上 GBhealth = defaultdict(lambda: defaultdict(float))for _, el in ET.iterparse(EXPORT): kind = WANTED.get(el.get("type")) if el.tag == "Record" else None if kind: t0 = datetime.strptime(el.get("startDate"), "%Y-%m-%d %H:%M:%S %z") t1 = datetime.strptime(el.get("endDate"), "%Y-%m-%d %H:%M:%S %z") if t1 >= START and t0 < END: mode = "sum" if kind in ("stand", "steps") else "mean" spread(health, kind, t0, t1, float(el.get("value")), mode) el.clear()
# 2. ActivityWatch:键鼠是否活跃 + 前台应用def aw_events(prefix): base = "http://localhost:5600/api/0/buckets/" buckets = json.load(urllib.request.urlopen(base)) bid = next(b for b in buckets if b.startswith(prefix)) q = urllib.parse.urlencode({"start": START.isoformat(), "end": END.isoformat(), "limit": -1}) for e in json.load(urllib.request.urlopen(f"{base}{bid}/events?{q}")): t0 = datetime.fromisoformat(e["timestamp"].replace("Z", "+00:00")) yield t0, t0 + timedelta(seconds=e["duration"]), e["data"]
active = [(t0, t1) for t0, t1, d in aw_events("aw-watcher-afk_") if d["status"] == "not-afk"]apps = defaultdict(lambda: defaultdict(float))for t0, t1, d in aw_events("aw-watcher-window_"): for a0, a1 in active: # 只算人在键盘前的那部分 lo, hi = max(t0, a0), min(t1, a1) for s in SLOTS: sec = overlap(s, s + SLOT, lo, hi) if lo < hi else 0 if sec: apps[s][d["app"]] += sec
def mean(pairs): return sum(v * w for v, w in pairs) / sum(w for _, w in pairs)
def db_mean(pairs): """分贝不能直接平均,先换回声能再平均(等效连续声级 Leq)。""" return 10 * math.log10(mean([(10 ** (v / 10), w) for v, w in pairs]))
# 3. 逐格输出,并按规则猜一个状态print("时间 屏幕 站立 步数 心率 噪声 前台应用 推断")total = defaultdict(float)for s in SLOTS: h = health[s] screen = sum(apps[s].values()) / 60 stand, steps = min(h["stand"], 15), h["steps"] hr = round(mean(h["hr_w"])) if "hr_w" in h else None noise = round(db_mean(h["noise_w"])) if "noise_w" in h else None top = max(apps[s], key=apps[s].get) if apps[s] else "-" if steps > 600: state = "走路/通勤" elif screen >= 10: # 推断:Mac 在用,手表没记到站立就当坐着 state = "屏幕前·坐" if stand <= 3 else "屏幕前·站过" elif stand >= 5: state = "站着/走动" elif hr is not None: state = "静止·离开屏幕" # 坐着开会、站着聊天、吃饭……这里分不出来 else: state = "没数据" # 多半是没戴表 total[state] += 15 total["屏幕"] += screen total["站立"] += stand print(f"{s:%H:%M} {screen:4.0f} {stand:4.0f} {steps:4.0f} " f"{hr or '-':>4} {noise or '-':>4} {top[:14]:<14} {state}")
print("\n合计(分钟):", {k: round(v) for k, v in total.items()})用模拟数据跑出来的一段输出长这样:
时间 屏幕 站立 步数 心率 噪声 前台应用 推断08:30 0 0 1650 69 48 - 走路/通勤09:00 15 0 0 72 45 Code 屏幕前·坐09:15 15 0 0 70 45 Code 屏幕前·坐10:00 15 5 0 69 66 Code 屏幕前·站过12:00 0 0 0 73 52 - 静止·离开屏幕...合计(分钟): {'静止·离开屏幕': 510, '屏幕': 480, '站立': 25, '走路/通勤': 30, '屏幕前·坐': 450, '屏幕前·站过': 30}规则部分故意写得很朴素,方便按自己的情况改:用升降桌的,可以把“屏幕前·站过”改成按站立分钟的比例拆开;有 Arc Timeline 导出的地点,可以把“静止·离开屏幕”按地点再细分成“会议室”“餐厅”。
参考资料
Footnotes
-
appleStandHourdeveloper.apple.com ↗每小时“站起来并活动至少 1 分钟”算一个站立小时https://developer.apple.com/documentation/healthkit/hkcategorytypeidentifier/applestandhour ↩
-
appleStandTimedeveloper.apple.com ↗iOS 13 / watchOS 6 起,累计型时间样本https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier/applestandtime ↩
-
About wrist detection on Apple Watchsupport.apple.com ↗关闭手腕检测后不记录站立,也没有后台心率https://support.apple.com/en-us/105002 ↩
-
CMMotionActivitydeveloper.apple.com ↗
stationary/walking/running/automotive/cycling/unknown,加confidencehttps://developer.apple.com/documentation/coremotion/cmmotionactivity ↩ -
queryActivityStarting(from:to:to:withHandler:)developer.apple.com ↗系统最多只保留最近 7 天的活动数据https://developer.apple.com/documentation/coremotion/cmmotionactivitymanager/queryactivitystarting(from:to:to:withhandler:) ↩
-
recordAccelerometer(forDuration:)developer.apple.com ↗50 Hz,单次最多 12 小时,数据保留 3 天https://developer.apple.com/documentation/coremotion/cmsensorrecorder/recordaccelerometer(forduration:) ↩
-
ActivityWatch Watchersdocs.activitywatch.net ↗
aw-watcher-input、aw-watcher-table(升降桌)、Life Cycle 导入等https://docs.activitywatch.net/en/latest/watchers.html ↩ ↩2 -
DeviceActivityReportdeveloper.apple.com ↗报表扩展跑在沙箱里,不能联网,也不能把数据带出扩展https://developer.apple.com/documentation/deviceactivity/deviceactivityreport ↩
-
Knowledge is Power! Using the macOS/iOS knowledgeC.db Database to Determine Precise User and Application Usagemac4n6.com ↗
/app/usage、/app/inFocus、/display/isBacklit等流https://www.mac4n6.com/blog/2018/8/5/knowledge-is-power-using-the-knowledgecdb-database-on-macos-and-ios-to-determine-precise-user-and-application-usage ↩ -
Bringing it Back with Biome Datamagnetforensics.com ↗应用使用记录迁到 Biome,SEGB 二进制格式https://www.magnetforensics.com/blog/bringing-it-back-with-biome-data/ ↩
-
Screen Time Integrationtimingapp.com ↗读屏幕使用时间数据库需要完全磁盘访问权限;系统界面只有小时粒度、保留四周https://timingapp.com/help/screen-time ↩ ↩2
-
secondsSinceLastEventType(_:eventType:)developer.apple.com ↗https://developer.apple.com/documentation/coregraphics/cgeventsource/secondssincelasteventtype(_:eventtype:) ↩
-
ActivityWatch FAQdocs.activitywatch.net ↗超过 3 分钟没有键鼠输入就记为 afk;读窗口标题需要辅助功能权限https://docs.activitywatch.net/en/latest/faq.html ↩ ↩2 ↩3
-
ActivityWatch REST APIdocs.activitywatch.net ↗https://docs.activitywatch.net/en/latest/api/rest.html ↩
-
screenpipegithub.com ↗https://github.com/screenpipe/screenpipe ↩
-
Monitor your heart rate with Apple Watchsupport.apple.com ↗https://support.apple.com/en-us/120277 ↩
-
environmentalAudioExposuredeveloper.apple.com ↗单位 dBASPL,离散的等效连续声级https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier/environmentalaudioexposure ↩
-
Measure noise levels with Apple Watchsupport.apple.com ↗全天周期性测量,只用麦克风测声级,不录音也不保存https://support.apple.com/en-us/102315 ↩
-
environmentalSoundReductiondeveloper.apple.com ↗https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier/environmentalsoundreduction ↩
-
timeInDaylightdeveloper.apple.com ↗iOS 17 / watchOS 10 起https://developer.apple.com/documentation/healthkit/hkquantitytypeidentifier/timeindaylight ↩
-
CLVisitdeveloper.apple.com ↗https://developer.apple.com/documentation/corelocation/clvisit ↩
-
SRSensordeveloper.apple.com ↗https://developer.apple.com/documentation/sensorkit/srsensor ↩
-
Configuring your project for sensor readingdeveloper.apple.com ↗只授予获批的研究项目https://developer.apple.com/documentation/sensorkit/configuring-your-project-for-sensor-reading ↩
-
SRFetchRequestdeveloper.apple.com ↗新数据有 24 小时冻结期https://developer.apple.com/documentation/sensorkit/srfetchrequest ↩
-
Health Auto Exporthealthyapps.dev ↗https://www.healthyapps.dev/ ↩ ↩2
-
How is Felix today?howisfelix.today ↗https://howisfelix.today ↩
-
FxLifeSheetgithub.com ↗https://github.com/KrauseFx/FxLifeSheet ↩
-
Limitlesslimitless.ai ↗被 Meta 收购,Rewind 已停止录屏与录音https://www.limitless.ai/ ↩