Home Assistant integration for eGym / Netpulse member accounts. It signs in to your gym's Netpulse portal, fetches your workout history and your bio-age profile, and exposes them as sensors: how often you trained, how hard, and what the last EGYM machine circuit actually looked like.
Not affiliated with eGym or Netpulse. This is an independent, unofficial integration built against an undocumented API — there is no public eGym API. It can be changed or withdrawn at any time, which will break this integration without warning. Your gym's terms of service may not permit automated access; that is your decision to make. The eGym and Netpulse names identify the service this talks to and belong to their owners.
It is not a medical device. Bio-age, body fat, BMI and blood pressure are whatever eGym measured or you typed into their app, carried across unchanged.
Around 40 sensors, all named sensor.egym_*.
How often you trained — three different questions, three different numbers:
| Entity | Notes |
|---|---|
sensor.egym_active_days_30_days, _7_days, _12_months |
Distinct calendar days with any activity. The number to trust |
sensor.egym_gym_days_30_days, _12_months |
Distinct days with a gym-machine workout |
sensor.egym_gym_workouts_30_days, _7_days |
Workouts recorded on a gym machine |
sensor.egym_workouts_30_days |
Raw rows — see the caveat below |
sensor.egym_days_since_last_workout, _gym_workout |
eGym mirrors Garmin, Strava and Apple Health into the same feed and files one workout per connected app for the same real activity, so a single bike ride appears three times. Raw workout counts therefore over-report by roughly 3x.
sensor.egym_workouts_30_dayscarries asourcesand anactivitiesbreakdown as attributes, which makes the double counting visible instead of leaving it as a footnote.
Totals: calories_30_days, activity_points_30_days (and _7_days),
training_time_30_days, distance_30_days, training_volume_30_days.
Most recent workout: last_workout (timestamp), last_workout_type,
_duration, _distance, _calories, _activity_points, _heart_rate.
Most recent gym workout: last_gym_workout (timestamp, with exercises,
sets, volume_kg and top_weight_kg attributes), _duration, _calories,
_activity_points, plus:
last_gym_workout_volume— reps × weight summed over every set, in kg. The single number that says whether a session was harder than the one before.unknownfor a pure-cardio session rather than a misleading0.last_gym_workout_top_weight— heaviest weight moved.
Bio-age and body values — bioage_total, _cardio, _metabolic,
_muscle, _flexibility (unknown until the studio's mobility test has been
done once), the three muscle regions _upper_body / _core / _lower_body
(each with a muscles_state attribute flagging a left/right imbalance), plus
body_fat, bmi, vo2max, resting_heart_rate and, once entered in the eGym
app, waist_to_hip_ratio and blood_pressure_systolic / _diastolic.
The total and the four component ages carry a change attribute — eGym's own
difference to the previous measurement in years, negative when the score got
younger — and a measured_at timestamp of the visit that set them.
These are health data and can be switched off entirely — see Options.
Bio-age is reported in whole years, spelled Jahre rather than the SI symbol
a: Home Assistant does not translate units, and 57 a reads as nothing at
all.
Every numeric sensor carries a state_class, so most of them accumulate
long-term statistics. They start empty and fill in from the day you install
this — eGym exposes no retrievable value history (see
docs/API.md), so there is nothing to backfill
them with.
Home Assistant 2025.1 or newer, and an eGym/Netpulse member account at a gym that uses it.
The button opens this repository in HACS on your own instance. Select Download, then restart Home Assistant.
It goes through My Home Assistant, which asks your browser for the address of your instance the first time and remembers it afterwards. Nothing is sent anywhere else — the page only redirects you. If you would rather not use it, the manual route below does exactly the same thing.
- HACS → three-dot menu → Custom repositories.
- Add
https://github.com/shadowempire123/ha-egymwith category Integration. - Download eGym, then restart Home Assistant.
Copy custom_components/egym into your Home Assistant config directory and
restart.
Or Settings → Devices & services → Add integration → eGym. Either way you need the restart first: Home Assistant only picks up a new integration at startup.
You need three things: your brand, your member username or email, and your password. The credentials are verified during setup.
The brand is your gym's Netpulse subdomain — mygym for
mygym.netpulse.com. docs/API.md lists the ways
to find it; the quickest is usually your club app's store listing, whose bundle
id follows com.netpulse.<brand>.
If eGym stops accepting your password, Home Assistant asks for a new one through the usual re-authentication prompt. The integration does not have to be removed and set up again — which matters here, because a service that rate-limits failed logins is not one you want an integration retrying a dead password against every fifteen minutes.
Settings → Devices & services → eGym → Configure
- Polling interval — 5 minutes to 6 hours, 15 minutes by default. The floor is enforced in code, not just in the form: see Rate limiting.
- Body and bio-age sensors — on by default. Switched off, those sensors are not created and the bio-age service is not called at all, so the data never reaches Home Assistant in the first place. See below for why you might want that.
Your training history and, unless you turn it off, a set of health measurements: bio-age, body fat, BMI, VO2max, resting heart rate and blood pressure. That is a different class of data from a light switch, so:
- It lands in the recorder database and in your backups. A Home Assistant
sensor's history is stored like any other, which means it is in
home-assistant_v2.dband in every backup of it — including backups that go to Home Assistant Cloud or a NAS. This is not specific to this integration, but it is worth knowing before you enable it. The Body and bio-age sensors option exists for exactly this; you can also exclude the entities fromrecorderinconfiguration.yamlif you want the current value without the history. - It is visible to every Home Assistant user who can see the entities. Home Assistant has no per-user entity permissions. A shared dashboard shows your body fat to whoever is looking at it.
- The device is named "eGym", not after you. The device name prefixes every
entity id, so naming it after the account would put an email address into
every
entity_idand onto every dashboard.
Only to https://<brand>.netpulse.com, form-encoded, over TLS.
The brand is user input that decides the hostname the password is posted to, so
it is validated against an allow list — lower-case letters, digits and hyphens,
in the shape of a DNS label — and nothing else is accepted. A deny list would
not do: a brand of evil.com#, attacker.example/x or user@evil.com all
move that host somewhere else, and the login request would carry your
credentials there. The check runs in the config flow and again when the entry
is loaded, so an entry written by an older version or edited by hand in
.storage cannot get around it either.
The password itself is stored where Home Assistant stores config entries, and is never written to the log. The session cookie is held in memory only. Error messages have your member id stripped out of them before they are raised, because they end up in the log and, during setup, on screen.
eGym rate-limits frequent requests, and repeated failed logins can get your address temporarily blocked — which affects your phone and the eGym app too, not just Home Assistant. Three things guard against that:
- One update is one request (two with body values on). The full 365 days come back in a single call, and the 7-, 30- and 365-day windows are derived from it.
- The polling interval has a floor of 300 seconds, applied when the entry is loaded rather than only in the options form.
- A refused login is not retried. It raises a re-authentication prompt, which also stops the polling until you answer it. An expired session is different and gets one silent login — that is the ordinary case.
Settings → Devices & services → eGym → the three dots → Download diagnostics produces a report designed to be pasted into a public issue.
It works the opposite way round from the usual redaction list: nothing is
included unless it is on an allow list of counts, and every other field is
reduced to its type rather than its value. "bioage_total": "int" tells a
maintainer the parser found a reading without telling anybody what your bio-age
is. The raw API responses are reduced to their shape — field names and types
survive, which is what identifies a renamed key, while the values do not. A
value-based scrub runs over the finished report as a second line of defence.
Your username and password are removed, and the per-exercise breakdown is reduced to a count — the names in it are your training profile, not anything the API defines. The connected-app breakdown (Garmin, Strava, Apple Health) is kept: it names a service rather than a person, and the triple counting those cause is what most reports are about.
The brand is kept, deliberately: it names the gym rather than you, and it is the one setting that differs between installations, so a report without it usually cannot be answered.
The brand images live in custom_components/egym/brand/ and are served by Home
Assistant itself through its brands proxy, at
/api/brands/integration/egym/icon.png. Local images take priority over the
brands CDN, so nothing has to be registered anywhere: since Home Assistant
2026.3 the brands repository no
longer accepts icons for custom integrations. On older versions the frontend
falls back to the CDN, which has no entry for this integration and answers with
a placeholder. Only the icon is affected either way.
The mark is a plain dumbbell drawn by scripts/make_brand_images.py, not
eGym's logo. Naming their product in a README is one thing; shipping their
trademark in a repository is another.
The test suite runs without a Home Assistant installation — the handful of
homeassistant and aiohttp names the modules need are stubbed in
tests/conftest.py:
pip install -r requirements_test.txt
pytestReleasing is a single step: bump version in manifest.json and VERSION in
const.py, add a CHANGELOG.md section for it, then push the matching v*
tag. A workflow publishes the release with those changelog entries as its
notes, and refuses the tag if the two versions disagree.
docs/API.md records what the undocumented API actually does, which endpoints were probed and what they answered, and what is still unsolved.
MIT — see LICENSE.