napper-sync 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. napper_sync-0.1.0/CHANGELOG.md +18 -0
  2. napper_sync-0.1.0/CONTRIBUTING.md +38 -0
  3. napper_sync-0.1.0/LICENSE +21 -0
  4. napper_sync-0.1.0/MANIFEST.in +8 -0
  5. napper_sync-0.1.0/PKG-INFO +223 -0
  6. napper_sync-0.1.0/README.md +194 -0
  7. napper_sync-0.1.0/docs/deployment.md +130 -0
  8. napper_sync-0.1.0/docs/famly-api.md +120 -0
  9. napper_sync-0.1.0/docs/napper-api.md +206 -0
  10. napper_sync-0.1.0/docs/releasing.md +52 -0
  11. napper_sync-0.1.0/examples/config.example.toml +25 -0
  12. napper_sync-0.1.0/examples/entries.example.csv +9 -0
  13. napper_sync-0.1.0/examples/entries.example.json +8 -0
  14. napper_sync-0.1.0/examples/systemd/napper-sync-failed@.service +14 -0
  15. napper_sync-0.1.0/examples/systemd/napper-sync.service +29 -0
  16. napper_sync-0.1.0/examples/systemd/napper-sync.timer +17 -0
  17. napper_sync-0.1.0/examples/windows-task.ps1 +6 -0
  18. napper_sync-0.1.0/pyproject.toml +55 -0
  19. napper_sync-0.1.0/setup.cfg +4 -0
  20. napper_sync-0.1.0/src/napper_sync/__init__.py +7 -0
  21. napper_sync-0.1.0/src/napper_sync/categories.py +314 -0
  22. napper_sync-0.1.0/src/napper_sync/cli.py +389 -0
  23. napper_sync-0.1.0/src/napper_sync/config.py +140 -0
  24. napper_sync-0.1.0/src/napper_sync/famly.py +157 -0
  25. napper_sync-0.1.0/src/napper_sync/famly_mapping.py +130 -0
  26. napper_sync-0.1.0/src/napper_sync/napper.py +451 -0
  27. napper_sync-0.1.0/src/napper_sync/notify.py +94 -0
  28. napper_sync-0.1.0/src/napper_sync/sources.py +81 -0
  29. napper_sync-0.1.0/src/napper_sync/sync.py +270 -0
  30. napper_sync-0.1.0/src/napper_sync.egg-info/PKG-INFO +223 -0
  31. napper_sync-0.1.0/src/napper_sync.egg-info/SOURCES.txt +45 -0
  32. napper_sync-0.1.0/src/napper_sync.egg-info/dependency_links.txt +1 -0
  33. napper_sync-0.1.0/src/napper_sync.egg-info/entry_points.txt +2 -0
  34. napper_sync-0.1.0/src/napper_sync.egg-info/requires.txt +4 -0
  35. napper_sync-0.1.0/src/napper_sync.egg-info/top_level.txt +1 -0
  36. napper_sync-0.1.0/tests/conftest.py +186 -0
  37. napper_sync-0.1.0/tests/test_categories.py +76 -0
  38. napper_sync-0.1.0/tests/test_famly_client.py +44 -0
  39. napper_sync-0.1.0/tests/test_famly_mapping.py +163 -0
  40. napper_sync-0.1.0/tests/test_napper_client.py +169 -0
  41. napper_sync-0.1.0/tests/test_notify_and_cli.py +122 -0
  42. napper_sync-0.1.0/tests/test_probe_corrections.py +86 -0
  43. napper_sync-0.1.0/tests/test_sync.py +308 -0
  44. napper_sync-0.1.0/tools/analyze_categories.py +100 -0
  45. napper_sync-0.1.0/tools/probe_corrections.py +167 -0
  46. napper_sync-0.1.0/tools/probe_write_path.py +159 -0
  47. napper_sync-0.1.0/tools/roundtrip_categories.py +131 -0
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-11-08
4
+
5
+ First public release.
6
+
7
+ - Napper client: OTP login, automatic token refresh, read/write/delete for all
8
+ eleven log categories, local field validation, 429 back-off.
9
+ - Famly client and mapping: calendar endpoint, sleep/meal/diaper → Napper,
10
+ unknown event types reported instead of dropped.
11
+ - Sync core with duplicate detection (10-minute fuzz, span overlap, one-per-day
12
+ markers), dry-run by default.
13
+ - Corrections: Famly entries carry a deterministic Napper ID, so a later change
14
+ at the nursery (end time, note, diaper content) updates the entry instead of
15
+ being skipped. CSV/JSON rows may pin an `id` for the same effect.
16
+ - CSV/JSON import.
17
+ - Discord notifications; systemd units and a Windows task example.
18
+ - Documentation of both APIs as observed.
@@ -0,0 +1,38 @@
1
+ # Contributing
2
+
3
+ Thanks for taking a look. Two things matter most here, because both APIs are
4
+ undocumented and change without notice:
5
+
6
+ 1. **Verify against the real API before changing a request shape.** The mock
7
+ in `tests/conftest.py` reproduces what was observed; it cannot tell you
8
+ that Napper changed. `tools/roundtrip_categories.py` can - it writes one
9
+ entry per category far in the past and cleans up after itself.
10
+ 2. **Never commit anything from `~/.config/napper-sync/`**, and strip IDs,
11
+ names and tokens from any API output you paste into an issue. Child IDs
12
+ and account IDs are UUIDs; the `.gitignore` catches the obvious files but
13
+ not a pasted JSON blob.
14
+
15
+ ## Setup
16
+
17
+ ```bash
18
+ pip install -e ".[dev]"
19
+ pytest
20
+ ruff check . && ruff format .
21
+ ```
22
+
23
+ ## Adding a Famly event type
24
+
25
+ 1. Capture one real event with `napper-sync famly calendar <from> <to> --json`
26
+ and anonymise it (replace the child's name in `title`, drop `originator`
27
+ and `invitees`).
28
+ 2. Add a branch in `famly_mapping.translate()` and a test in
29
+ `tests/test_famly_mapping.py` using the anonymised event.
30
+ 3. If Napper needs a category or field that `categories.py` does not have,
31
+ confirm the field name against your own history
32
+ (`tools/analyze_categories.py`) first.
33
+
34
+ ## Style
35
+
36
+ Ruff handles formatting and linting (`ruff format`, `ruff check`). Keep the
37
+ standard-library-only rule for the package itself; dev tooling may use
38
+ anything.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Wuestling
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ # Make the source distribution self-contained: the tests need conftest.py and
2
+ # reach into tools/, and packagers want the docs and examples next to the code.
3
+ graft tests
4
+ graft tools
5
+ graft docs
6
+ graft examples
7
+ include CHANGELOG.md CONTRIBUTING.md
8
+ global-exclude __pycache__ *.py[cod] .coverage
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: napper-sync
3
+ Version: 0.1.0
4
+ Summary: Unofficial Napper baby-tracker API client, and a sync that imports what the nursery logs in Famly.
5
+ Author: Patrick Wuestling
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/PW-CODING/napper-sync
8
+ Project-URL: Issues, https://github.com/PW-CODING/napper-sync/issues
9
+ Project-URL: Documentation, https://github.com/PW-CODING/napper-sync/tree/main/docs
10
+ Project-URL: Changelog, https://github.com/PW-CODING/napper-sync/blob/main/CHANGELOG.md
11
+ Keywords: napper,famly,baby,baby-tracker,sleep-tracker,nursery,daycare,childcare,kita,krippe,sync,api-client
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Home Automation
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: ruff>=0.6; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # napper-sync
31
+
32
+ **Sync what your nursery logs in [Famly](https://famly.co) - naps, meals, diaper
33
+ changes - into the [Napper](https://napper.app) baby tracker, automatically. Also an
34
+ unofficial Python client for the Napper API.**
35
+
36
+ [![CI](https://github.com/PW-CODING/napper-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/PW-CODING/napper-sync/actions/workflows/ci.yml)
37
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)
38
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
39
+
40
+ **Auf Deutsch:** Überträgt automatisch, was die Kita in Famly einträgt - Schlaf, Mahlzeiten
41
+ und Windelwechsel - in die Baby-App Napper. Kein Abtippen am Abend, keine Duplikate, und
42
+ nachträgliche Korrekturen der Kita werden übernommen. Läuft per Zeitplan auf einem
43
+ Raspberry Pi oder jedem anderen Rechner und braucht nur die Python-Standardbibliothek.
44
+ Die Dokumentation ist englisch, Fragen und Issues gern auch auf Deutsch.
45
+
46
+ Napper is a sleep-and-care tracker for babies. Famly is the app many nurseries
47
+ use to tell parents what happened during the day. If your child is in a
48
+ Famly nursery and you track at home with Napper, you end up typing the
49
+ nursery's entries into Napper by hand every evening. This tool does that for
50
+ you, every hour, without creating duplicates.
51
+
52
+ Along the way it documents two APIs that have no public documentation - see
53
+ [`docs/`](https://github.com/PW-CODING/napper-sync/tree/main/docs).
54
+
55
+ > **Disclaimer.** This project is not affiliated with, endorsed by, or supported by
56
+ > Napper AB or Famly ApS. It talks to private, undocumented APIs that the official
57
+ > apps use internally. Those APIs may change or disappear with any app update, and
58
+ > using them may not be covered by the services' terms - check the terms that apply
59
+ > to your account before you decide. Use it with your own account, for your own
60
+ > child, at your own risk; how you use it is your responsibility.
61
+
62
+ ## What it does
63
+
64
+ - **Napper API client** - log in with the e-mailed one-time code, read the
65
+ history, write and delete entries in all eleven categories Napper knows
66
+ (nap, bedtime, woke up, night waking, nursing, bottle, solids, pumping,
67
+ diaper, temperature, medicine), with field validation before anything is sent.
68
+ - **Famly → Napper sync** - reads the nursery's daily events and creates the
69
+ matching Napper entries, marked `(Famly)` so you can tell them apart. When
70
+ the nursery corrects an entry later, the Napper entry follows.
71
+ - **Safe by default** - every write is a dry run unless you pass `--live`;
72
+ duplicate detection makes repeated runs harmless; the Napper rate limit is
73
+ handled with back-off.
74
+ - **File import** - CSV or JSON for anything else you want to get into Napper.
75
+ - **Unattended operation** - systemd timer, Discord notifications on failure
76
+ and on successful imports, tokens refreshed automatically.
77
+
78
+ Standard library only; no runtime dependencies.
79
+
80
+ ## Install
81
+
82
+ ```bash
83
+ pipx install git+https://github.com/PW-CODING/napper-sync
84
+ # or
85
+ pip install --user git+https://github.com/PW-CODING/napper-sync
86
+ ```
87
+
88
+ Requires Python 3.11+.
89
+
90
+ ## Quick start
91
+
92
+ ```bash
93
+ napper-sync login you@example.com # Napper e-mails a code; paste it
94
+ napper-sync babies # find your baby's ID
95
+ napper-sync config init # write ~/.config/napper-sync/config.toml
96
+ # -> put the baby ID into [napper] baby_id
97
+
98
+ napper-sync logs 2026-09-01 2026-09-07 # read a week
99
+ napper-sync add nap 2026-09-18T13:00 2026-09-18T14:20 # dry run
100
+ napper-sync add nap 2026-09-18T13:00 2026-09-18T14:20 --live # write
101
+ napper-sync add diaper 2026-09-18T15:30 --set diaperContent=WET --live
102
+ napper-sync add medicine 2026-09-18T19:00 --comment "Vitamin D" --live
103
+ napper-sync categories # every category, field and allowed value
104
+ ```
105
+
106
+ ### Famly sync
107
+
108
+ ```bash
109
+ napper-sync famly token <VALUE> # from localStorage["famly.accessToken"] in a logged-in browser
110
+ napper-sync famly children # -> [famly] child_id in the config
111
+ napper-sync famly calendar 2026-09-14 2026-09-18 # what the nursery logged
112
+
113
+ napper-sync sync-famly # dry run: shows what would be written
114
+ napper-sync sync-famly --live # write it
115
+ ```
116
+
117
+ To run this hourly on a Raspberry Pi with Discord alerts, see
118
+ [`docs/deployment.md`](https://github.com/PW-CODING/napper-sync/blob/main/docs/deployment.md).
119
+
120
+ ### Importing a file
121
+
122
+ ```bash
123
+ napper-sync sync-file entries.csv --live
124
+ ```
125
+
126
+ Columns `start,end,category,comment` plus any category field as a further
127
+ column; see [`examples/entries.example.csv`](https://github.com/PW-CODING/napper-sync/blob/main/examples/entries.example.csv).
128
+ An optional `id` column pins the Napper ID, so importing a corrected file
129
+ updates the entries instead of duplicating them.
130
+ Categories accept English or German short names (`nap`/`nickerchen`,
131
+ `diaper`/`windel`, `bottle`/`flasche`, …).
132
+
133
+ ## How it stays safe
134
+
135
+ | Risk | Answer |
136
+ |---|---|
137
+ | Writing by accident | every write and delete is a dry run without `--live` |
138
+ | Duplicates from repeated runs | Famly entries get a stable ID derived from child, category and start, so a repeated run rewrites the same entry instead of adding one; everything else is skipped if the same category exists within 10 minutes or overlaps; day markers are one-per-day |
139
+ | Corrections at the nursery | a changed end time, note or diaper content overwrites the Napper entry on the next run; fields added by hand in the app are kept |
140
+ | Hammering the API | 429 responses are parsed and retried with the server's requested delay |
141
+ | Bad data | field names, enum values and ranges are validated locally before a request is built |
142
+ | Silent breakage | Discord message on expired token, failed write, crashed run; unknown Famly event types are listed, never dropped |
143
+
144
+ ## Project layout
145
+
146
+ ```
147
+ src/napper_sync/
148
+ napper.py Napper API client (auth, read, write, delete)
149
+ categories.py what each category expects - the data model
150
+ sync.py Entry, duplicate detection, the sync loop
151
+ sources.py CSV / JSON sources
152
+ famly.py Famly API client
153
+ famly_mapping.py Famly events -> Napper entries
154
+ notify.py Discord webhook
155
+ config.py ~/.config/napper-sync/
156
+ cli.py the napper-sync command
157
+ tools/ research scripts: analyse history, rediscover endpoints, round-trip and correction checks
158
+ examples/ config, data files, systemd units, Windows task
159
+ docs/ what is known about both APIs, and how to deploy
160
+ tests/ pytest suite against an in-process mock of the Napper API
161
+ ```
162
+
163
+ ## Documentation
164
+
165
+ - [docs/napper-api.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/napper-api.md) - endpoints, auth, the log object,
166
+ all categories and fields, the traps (seconds in a field called minutes,
167
+ percent-encoded comments, date-based IDs), and how the write path was found
168
+ - [docs/famly-api.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/famly-api.md) - instances, token, the calendar
169
+ endpoint and its event shapes
170
+ - [docs/deployment.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/deployment.md) - systemd timer, hardening, Discord
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ git clone https://github.com/PW-CODING/napper-sync && cd napper-sync
176
+ pip install -e ".[dev]"
177
+ pytest
178
+ ruff check . && ruff format --check .
179
+ ```
180
+
181
+ The tests run against an in-process HTTP mock that reproduces the real API's
182
+ quirks (whole-day responses on write, 429 throttling, token refresh). Nothing
183
+ touches a real account.
184
+
185
+ Tools under `tools/` do talk to the real API and are meant for the day an app
186
+ update breaks something: `probe_write_path.py` rediscovers the write endpoint,
187
+ `roundtrip_categories.py` writes one entry per category far in the past,
188
+ checks it and deletes it again, `analyze_categories.py` summarises which
189
+ fields your own history actually uses, and `probe_corrections.py` checks that
190
+ a correction made later at the nursery updates the Napper entry in place.
191
+
192
+ ## Contributing
193
+
194
+ Issues and pull requests are welcome, in English or German. If an app update
195
+ changed something, the most useful report includes the output of
196
+ `tools/roundtrip_categories.py` or of `napper-sync famly calendar` for a day
197
+ with the new event type (with names and IDs removed).
198
+
199
+ ## Related projects
200
+
201
+ Community projects around the same two apps, none of which connects them:
202
+
203
+ - [home-assistant-napper](https://github.com/g-nogueira/home-assistant-napper) -
204
+ read-only Napper integration for Home Assistant
205
+ - [ha-napper](https://github.com/bbruenings/ha-napper) - another Home Assistant
206
+ integration for Napper
207
+ - [napper-export](https://github.com/brittraee/napper-export) - export a child's
208
+ complete Napper history
209
+ - [famly-fetch](https://github.com/jacobbunk/famly-fetch) - download your child's
210
+ photos from Famly
211
+
212
+ ## Acknowledgements
213
+
214
+ Reading the Napper API was first worked out by
215
+ [brittraee/napper-export](https://github.com/brittraee/napper-export) and
216
+ [g-nogueira/home-assistant-napper](https://github.com/g-nogueira/home-assistant-napper);
217
+ the OTP login flow and request headers here follow their findings. Writing,
218
+ deleting, the category model and the Famly side were worked out for this
219
+ project.
220
+
221
+ ## License
222
+
223
+ [MIT](https://github.com/PW-CODING/napper-sync/blob/main/LICENSE)
@@ -0,0 +1,194 @@
1
+ # napper-sync
2
+
3
+ **Sync what your nursery logs in [Famly](https://famly.co) - naps, meals, diaper
4
+ changes - into the [Napper](https://napper.app) baby tracker, automatically. Also an
5
+ unofficial Python client for the Napper API.**
6
+
7
+ [![CI](https://github.com/PW-CODING/napper-sync/actions/workflows/ci.yml/badge.svg)](https://github.com/PW-CODING/napper-sync/actions/workflows/ci.yml)
8
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)
9
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
10
+
11
+ **Auf Deutsch:** Überträgt automatisch, was die Kita in Famly einträgt - Schlaf, Mahlzeiten
12
+ und Windelwechsel - in die Baby-App Napper. Kein Abtippen am Abend, keine Duplikate, und
13
+ nachträgliche Korrekturen der Kita werden übernommen. Läuft per Zeitplan auf einem
14
+ Raspberry Pi oder jedem anderen Rechner und braucht nur die Python-Standardbibliothek.
15
+ Die Dokumentation ist englisch, Fragen und Issues gern auch auf Deutsch.
16
+
17
+ Napper is a sleep-and-care tracker for babies. Famly is the app many nurseries
18
+ use to tell parents what happened during the day. If your child is in a
19
+ Famly nursery and you track at home with Napper, you end up typing the
20
+ nursery's entries into Napper by hand every evening. This tool does that for
21
+ you, every hour, without creating duplicates.
22
+
23
+ Along the way it documents two APIs that have no public documentation - see
24
+ [`docs/`](https://github.com/PW-CODING/napper-sync/tree/main/docs).
25
+
26
+ > **Disclaimer.** This project is not affiliated with, endorsed by, or supported by
27
+ > Napper AB or Famly ApS. It talks to private, undocumented APIs that the official
28
+ > apps use internally. Those APIs may change or disappear with any app update, and
29
+ > using them may not be covered by the services' terms - check the terms that apply
30
+ > to your account before you decide. Use it with your own account, for your own
31
+ > child, at your own risk; how you use it is your responsibility.
32
+
33
+ ## What it does
34
+
35
+ - **Napper API client** - log in with the e-mailed one-time code, read the
36
+ history, write and delete entries in all eleven categories Napper knows
37
+ (nap, bedtime, woke up, night waking, nursing, bottle, solids, pumping,
38
+ diaper, temperature, medicine), with field validation before anything is sent.
39
+ - **Famly → Napper sync** - reads the nursery's daily events and creates the
40
+ matching Napper entries, marked `(Famly)` so you can tell them apart. When
41
+ the nursery corrects an entry later, the Napper entry follows.
42
+ - **Safe by default** - every write is a dry run unless you pass `--live`;
43
+ duplicate detection makes repeated runs harmless; the Napper rate limit is
44
+ handled with back-off.
45
+ - **File import** - CSV or JSON for anything else you want to get into Napper.
46
+ - **Unattended operation** - systemd timer, Discord notifications on failure
47
+ and on successful imports, tokens refreshed automatically.
48
+
49
+ Standard library only; no runtime dependencies.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pipx install git+https://github.com/PW-CODING/napper-sync
55
+ # or
56
+ pip install --user git+https://github.com/PW-CODING/napper-sync
57
+ ```
58
+
59
+ Requires Python 3.11+.
60
+
61
+ ## Quick start
62
+
63
+ ```bash
64
+ napper-sync login you@example.com # Napper e-mails a code; paste it
65
+ napper-sync babies # find your baby's ID
66
+ napper-sync config init # write ~/.config/napper-sync/config.toml
67
+ # -> put the baby ID into [napper] baby_id
68
+
69
+ napper-sync logs 2026-09-01 2026-09-07 # read a week
70
+ napper-sync add nap 2026-09-18T13:00 2026-09-18T14:20 # dry run
71
+ napper-sync add nap 2026-09-18T13:00 2026-09-18T14:20 --live # write
72
+ napper-sync add diaper 2026-09-18T15:30 --set diaperContent=WET --live
73
+ napper-sync add medicine 2026-09-18T19:00 --comment "Vitamin D" --live
74
+ napper-sync categories # every category, field and allowed value
75
+ ```
76
+
77
+ ### Famly sync
78
+
79
+ ```bash
80
+ napper-sync famly token <VALUE> # from localStorage["famly.accessToken"] in a logged-in browser
81
+ napper-sync famly children # -> [famly] child_id in the config
82
+ napper-sync famly calendar 2026-09-14 2026-09-18 # what the nursery logged
83
+
84
+ napper-sync sync-famly # dry run: shows what would be written
85
+ napper-sync sync-famly --live # write it
86
+ ```
87
+
88
+ To run this hourly on a Raspberry Pi with Discord alerts, see
89
+ [`docs/deployment.md`](https://github.com/PW-CODING/napper-sync/blob/main/docs/deployment.md).
90
+
91
+ ### Importing a file
92
+
93
+ ```bash
94
+ napper-sync sync-file entries.csv --live
95
+ ```
96
+
97
+ Columns `start,end,category,comment` plus any category field as a further
98
+ column; see [`examples/entries.example.csv`](https://github.com/PW-CODING/napper-sync/blob/main/examples/entries.example.csv).
99
+ An optional `id` column pins the Napper ID, so importing a corrected file
100
+ updates the entries instead of duplicating them.
101
+ Categories accept English or German short names (`nap`/`nickerchen`,
102
+ `diaper`/`windel`, `bottle`/`flasche`, …).
103
+
104
+ ## How it stays safe
105
+
106
+ | Risk | Answer |
107
+ |---|---|
108
+ | Writing by accident | every write and delete is a dry run without `--live` |
109
+ | Duplicates from repeated runs | Famly entries get a stable ID derived from child, category and start, so a repeated run rewrites the same entry instead of adding one; everything else is skipped if the same category exists within 10 minutes or overlaps; day markers are one-per-day |
110
+ | Corrections at the nursery | a changed end time, note or diaper content overwrites the Napper entry on the next run; fields added by hand in the app are kept |
111
+ | Hammering the API | 429 responses are parsed and retried with the server's requested delay |
112
+ | Bad data | field names, enum values and ranges are validated locally before a request is built |
113
+ | Silent breakage | Discord message on expired token, failed write, crashed run; unknown Famly event types are listed, never dropped |
114
+
115
+ ## Project layout
116
+
117
+ ```
118
+ src/napper_sync/
119
+ napper.py Napper API client (auth, read, write, delete)
120
+ categories.py what each category expects - the data model
121
+ sync.py Entry, duplicate detection, the sync loop
122
+ sources.py CSV / JSON sources
123
+ famly.py Famly API client
124
+ famly_mapping.py Famly events -> Napper entries
125
+ notify.py Discord webhook
126
+ config.py ~/.config/napper-sync/
127
+ cli.py the napper-sync command
128
+ tools/ research scripts: analyse history, rediscover endpoints, round-trip and correction checks
129
+ examples/ config, data files, systemd units, Windows task
130
+ docs/ what is known about both APIs, and how to deploy
131
+ tests/ pytest suite against an in-process mock of the Napper API
132
+ ```
133
+
134
+ ## Documentation
135
+
136
+ - [docs/napper-api.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/napper-api.md) - endpoints, auth, the log object,
137
+ all categories and fields, the traps (seconds in a field called minutes,
138
+ percent-encoded comments, date-based IDs), and how the write path was found
139
+ - [docs/famly-api.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/famly-api.md) - instances, token, the calendar
140
+ endpoint and its event shapes
141
+ - [docs/deployment.md](https://github.com/PW-CODING/napper-sync/blob/main/docs/deployment.md) - systemd timer, hardening, Discord
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ git clone https://github.com/PW-CODING/napper-sync && cd napper-sync
147
+ pip install -e ".[dev]"
148
+ pytest
149
+ ruff check . && ruff format --check .
150
+ ```
151
+
152
+ The tests run against an in-process HTTP mock that reproduces the real API's
153
+ quirks (whole-day responses on write, 429 throttling, token refresh). Nothing
154
+ touches a real account.
155
+
156
+ Tools under `tools/` do talk to the real API and are meant for the day an app
157
+ update breaks something: `probe_write_path.py` rediscovers the write endpoint,
158
+ `roundtrip_categories.py` writes one entry per category far in the past,
159
+ checks it and deletes it again, `analyze_categories.py` summarises which
160
+ fields your own history actually uses, and `probe_corrections.py` checks that
161
+ a correction made later at the nursery updates the Napper entry in place.
162
+
163
+ ## Contributing
164
+
165
+ Issues and pull requests are welcome, in English or German. If an app update
166
+ changed something, the most useful report includes the output of
167
+ `tools/roundtrip_categories.py` or of `napper-sync famly calendar` for a day
168
+ with the new event type (with names and IDs removed).
169
+
170
+ ## Related projects
171
+
172
+ Community projects around the same two apps, none of which connects them:
173
+
174
+ - [home-assistant-napper](https://github.com/g-nogueira/home-assistant-napper) -
175
+ read-only Napper integration for Home Assistant
176
+ - [ha-napper](https://github.com/bbruenings/ha-napper) - another Home Assistant
177
+ integration for Napper
178
+ - [napper-export](https://github.com/brittraee/napper-export) - export a child's
179
+ complete Napper history
180
+ - [famly-fetch](https://github.com/jacobbunk/famly-fetch) - download your child's
181
+ photos from Famly
182
+
183
+ ## Acknowledgements
184
+
185
+ Reading the Napper API was first worked out by
186
+ [brittraee/napper-export](https://github.com/brittraee/napper-export) and
187
+ [g-nogueira/home-assistant-napper](https://github.com/g-nogueira/home-assistant-napper);
188
+ the OTP login flow and request headers here follow their findings. Writing,
189
+ deleting, the category model and the Famly side were worked out for this
190
+ project.
191
+
192
+ ## License
193
+
194
+ [MIT](https://github.com/PW-CODING/napper-sync/blob/main/LICENSE)
@@ -0,0 +1,130 @@
1
+ # Running napper-sync unattended
2
+
3
+ The intended setup is a small always-on Linux box (a Raspberry Pi is plenty;
4
+ a run takes about two seconds of CPU) with a systemd timer. A Windows
5
+ scheduled task works too and is described at the end.
6
+
7
+ ## 1. Install
8
+
9
+ ```bash
10
+ # as the user that will run the sync (not root)
11
+ pipx install git+https://github.com/PW-CODING/napper-sync
12
+ # or: pip install --user git+https://github.com/PW-CODING/napper-sync
13
+ napper-sync --version
14
+ ```
15
+
16
+ Python 3.11 or newer is required (Debian 12 "bookworm" ships 3.11).
17
+
18
+ ## 2. Configure
19
+
20
+ ```bash
21
+ napper-sync config init # writes ~/.config/napper-sync/config.toml
22
+ napper-sync login you@example.com # e-mailed one-time code
23
+ napper-sync babies # -> [napper] baby_id
24
+ napper-sync famly token <VALUE> # see "Getting the Famly token"
25
+ napper-sync famly children # -> [famly] child_id
26
+ napper-sync config show # everything in place?
27
+ ```
28
+
29
+ Then dry-run it once and read the output:
30
+
31
+ ```bash
32
+ napper-sync sync-famly # shows what would be written
33
+ napper-sync sync-famly --live # writes
34
+ ```
35
+
36
+ ### Getting the Famly token
37
+
38
+ Log in to Famly in a desktop browser, open the developer tools (F12),
39
+ Console tab, and run:
40
+
41
+ ```js
42
+ JSON.parse(localStorage["famly.accessToken"])
43
+ ```
44
+
45
+ Copy the string (36 characters) into `napper-sync famly token <VALUE>`. If
46
+ your nursery is on a different Famly instance than `app.famly.de`, check
47
+ `localStorage["famly.existingLogins"]` and set `[famly] host` accordingly.
48
+
49
+ On a headless server, run the browser step on any machine and copy the token
50
+ across (`ssh host 'napper-sync famly token <VALUE>'`).
51
+
52
+ ### Secrets and permissions
53
+
54
+ Everything sensitive sits in `~/.config/napper-sync/` with mode `600`:
55
+ `napper-tokens.json`, `famly-token`, and the Discord webhook inside
56
+ `config.toml`. Keep that directory out of backups that leave your control, and
57
+ never commit it.
58
+
59
+ ## 3. systemd timer
60
+
61
+ Copy the three units from `examples/systemd/`, adjust `User=` and the paths in
62
+ `ExecStart=` / `ReadWritePaths=` to your installation, then:
63
+
64
+ ```bash
65
+ sudo install -m 644 examples/systemd/napper-sync.service \
66
+ examples/systemd/napper-sync.timer \
67
+ 'examples/systemd/napper-sync-failed@.service' /etc/systemd/system/
68
+ sudo systemctl daemon-reload
69
+ sudo systemctl enable --now napper-sync.timer
70
+
71
+ systemctl list-timers napper-sync.timer # next run
72
+ sudo systemctl start napper-sync.service # run once, now
73
+ journalctl -u napper-sync -n 30 --no-pager # what happened
74
+ ```
75
+
76
+ Design notes, in case you want to change them:
77
+
78
+ * **`OnCalendar=Mon..Fri 07..18:00`** - hourly during nursery hours plus one
79
+ run after closing. Nothing is logged at night or on weekends, so nothing
80
+ runs. `systemd-analyze calendar 'Mon..Fri 07..18:00'` previews the schedule.
81
+ * **`[sync] days = 4`** - each run looks four days back. Three would make the
82
+ Monday run reach exactly to Friday; four leaves room for entries added
83
+ Friday evening.
84
+ * **`Persistent=true`** - runs missed while the machine was off are caught up
85
+ on boot.
86
+ * **`OnFailure=`** - if the process crashes or times out, a second unit posts
87
+ the last journal lines to Discord. API-level failures (expired token,
88
+ unreachable host) are reported by the sync itself.
89
+ * **Hardening** - `ProtectSystem=strict` and `ProtectHome=read-only`, with
90
+ `ReadWritePaths=` opened only for the config directory. That last line is
91
+ not optional: the Napper id-token is refreshed automatically and written
92
+ back to `napper-tokens.json`. Without write access the refresh fails
93
+ silently a few weeks after installation.
94
+
95
+ ## 4. Discord notifications
96
+
97
+ Create a webhook in your Discord server (channel settings → Integrations →
98
+ Webhooks), put the URL into `[notify] discord_webhook`, and test:
99
+
100
+ ```bash
101
+ napper-sync notify # "Test message"
102
+ ```
103
+
104
+ You will then hear about:
105
+
106
+ * an expired Famly token (with the command to fix it),
107
+ * Famly or Napper being unreachable or rejecting a write,
108
+ * a crashed or timed-out run (via the `OnFailure=` unit, journal attached),
109
+ * every run that imported or corrected something, with the list of entries.
110
+
111
+ A run that finds nothing new stays silent.
112
+
113
+ What the webhook cannot tell you: that the machine is off or offline. Silence
114
+ is not proof that all is well.
115
+
116
+ ## Windows instead of Linux
117
+
118
+ `examples/windows-task.ps1` registers an hourly scheduled task that runs
119
+ `napper-sync sync-famly --live`. Output goes nowhere by default; if you want
120
+ a log, wrap the call in a `.cmd` that appends to a file. The Discord
121
+ notifications work the same way.
122
+
123
+ ## Updating
124
+
125
+ ```bash
126
+ pipx upgrade napper-sync # or: pip install --user -U git+https://github.com/PW-CODING/napper-sync
127
+ sudo systemctl start napper-sync.service && journalctl -u napper-sync -n 20
128
+ ```
129
+
130
+ Config and tokens are untouched by upgrades.