Skip to content

Commit 163f89f

Browse files
mikejhillCopilot
andcommitted
feat: add 8 sensors, 3 services, and README example values
New sensors from existing model fields (no new API calls): - ftp_source, country (profile) - next_event_time (events) - career_total_elevation, career_total_time, career_total_activities, career_achievements, career_trophies (career) New services wired from existing API methods: - rouvy.register_challenge - rouvy.register_event - rouvy.unregister_event README updated with example values for all 41 sensors and full 6-service reference table. 457 tests passing, ruff clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2070ad2 commit 163f89f

File tree

8 files changed

+404
-63
lines changed

8 files changed

+404
-63
lines changed

README.md

Lines changed: 72 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
A custom [Home Assistant](https://www.home-assistant.io/) integration for
1111
the [Rouvy](https://rouvy.com/) indoor cycling platform, installable via
12-
[HACS](https://hacs.xyz/). Exposes 33 sensors and 3 services covering your
12+
[HACS](https://hacs.xyz/). Exposes 41 sensors and 6 services covering your
1313
Rouvy profile, activity history, training zones, challenges, routes, events,
1414
career progress, and social data.
1515

@@ -33,96 +33,107 @@ All sensors are created under the `sensor.rouvy_*` entity namespace.
3333

3434
### Profile
3535

36-
| Sensor | Unit | Description |
37-
| --- | --- | --- |
38-
| `weight` | kg | Current body weight |
39-
| `height` | cm | Current height |
40-
| `ftp` | W | Functional Threshold Power |
41-
| `max_heart_rate` | bpm | Maximum heart rate |
42-
| `units` || Preferred unit system (METRIC / IMPERIAL) |
43-
| `name` || Display name |
36+
| Sensor | Unit | Example | Description |
37+
| --- | --- | --- | --- |
38+
| `weight` | kg | `85.5` | Current body weight |
39+
| `height` | cm | `178.0` | Current height |
40+
| `ftp` | W | `250` | Functional Threshold Power |
41+
| `max_heart_rate` | bpm | `185` | Maximum heart rate |
42+
| `units` || `METRIC` | Preferred unit system |
43+
| `name` || `John Doe` | Display name |
44+
| `ftp_source` || `MANUAL` | How FTP was determined (MANUAL / AUTO) |
45+
| `country` || `US` | Account country code |
4446

4547
### Weekly Activity Stats
4648

4749
Current-week ride totals, refreshed each update cycle.
4850

49-
| Sensor | Unit | Description |
50-
| --- | --- | --- |
51-
| `weekly_distance` | km | Total ride distance this week |
52-
| `weekly_elevation` | m | Total elevation gain this week |
53-
| `weekly_calories` | kcal | Total calories burned this week |
54-
| `weekly_ride_time` | min | Total ride time this week |
55-
| `weekly_ride_count` || Number of rides this week |
56-
| `weekly_training_score` || Cumulative training score this week |
51+
| Sensor | Unit | Example | Description |
52+
| --- | --- | --- | --- |
53+
| `weekly_distance` | km | `142.3` | Total ride distance this week |
54+
| `weekly_elevation` | m | `1,850` | Total elevation gain this week |
55+
| `weekly_calories` | kcal | `3,200` | Total calories burned this week |
56+
| `weekly_ride_time` | min | `285` | Total ride time this week |
57+
| `weekly_ride_count` || `5` | Number of rides this week |
58+
| `weekly_training_score` || `312` | Cumulative training score this week |
5759

5860
### Last Activity
5961

60-
| Sensor | Unit | Description |
61-
| --- | --- | --- |
62-
| `last_activity_title` || Title of the most recent ride |
63-
| `last_activity_distance` | km | Distance of the most recent ride |
64-
| `last_activity_duration` | min | Duration of the most recent ride |
65-
| `last_activity_date` | timestamp | Start time of the most recent ride |
66-
| `total_activities` || Total number of recent activities |
62+
| Sensor | Unit | Example | Description |
63+
| --- | --- | --- | --- |
64+
| `last_activity_title` || `Col du Galibier` | Title of the most recent ride |
65+
| `last_activity_distance` | km | `34.2` | Distance of the most recent ride |
66+
| `last_activity_duration` | min | `62` | Duration of the most recent ride |
67+
| `last_activity_date` | timestamp | `2026-04-10T07:30:00Z` | Start time of the most recent ride |
68+
| `total_activities` || `247` | Total number of recent activities |
6769

6870
### Challenges
6971

70-
| Sensor | Unit | Description |
71-
| --- | --- | --- |
72-
| `active_challenges` || Number of currently active challenges |
73-
| `completed_challenges` || Number of completed challenges |
72+
| Sensor | Unit | Example | Description |
73+
| --- | --- | --- | --- |
74+
| `active_challenges` || `3` | Number of currently active challenges |
75+
| `completed_challenges` || `12` | Number of completed challenges |
7476

7577
### Training Zones
7678

77-
| Sensor | Unit | Description |
78-
| --- | --- | --- |
79-
| `power_zones` || Power zone boundaries (% of FTP) |
80-
| `hr_zones` || Heart rate zone boundaries (% of max HR) |
79+
| Sensor | Unit | Example | Description |
80+
| --- | --- | --- | --- |
81+
| `power_zones` || `[55, 75, 90, 105, 120]` | Power zone boundaries (% of FTP) |
82+
| `hr_zones` || `[60, 70, 80, 90]` | Heart rate zone boundaries (% of max HR) |
8183

8284
### Connected Apps
8385

84-
| Sensor | Unit | Description |
85-
| --- | --- | --- |
86-
| `connected_apps_count` || Total connected third-party apps |
87-
| `connected_apps_active` || Number of actively connected apps |
86+
| Sensor | Unit | Example | Description |
87+
| --- | --- | --- | --- |
88+
| `connected_apps_count` || `3` | Total connected third-party apps |
89+
| `connected_apps_active` || `2` | Number of actively connected apps |
8890

8991
### Routes
9092

91-
| Sensor | Unit | Description |
92-
| --- | --- | --- |
93-
| `favorite_routes_count` || Number of favorited routes |
94-
| `routes_online_riders` || Total online riders across favorites |
93+
| Sensor | Unit | Example | Description |
94+
| --- | --- | --- | --- |
95+
| `favorite_routes_count` || `15` | Number of favorited routes |
96+
| `routes_online_riders` || `42` | Total online riders across favorites |
9597

9698
### Events
9799

98-
| Sensor | Unit | Description |
99-
| --- | --- | --- |
100-
| `upcoming_events_count` || Number of upcoming events |
101-
| `next_event` || Title of the next scheduled event |
100+
| Sensor | Unit | Example | Description |
101+
| --- | --- | --- | --- |
102+
| `upcoming_events_count` || `2` | Number of upcoming events |
103+
| `next_event` || `Saturday Morning Race` | Title of the next scheduled event |
104+
| `next_event_time` || `2026-04-12T08:00:00Z` | Start time of the next scheduled event |
102105

103106
### Career
104107

105-
| Sensor | Unit | Description |
106-
| --- | --- | --- |
107-
| `career_level` || Current career level |
108-
| `total_xp` || Total experience points |
109-
| `total_coins` || Total coins earned |
110-
| `career_total_distance` | km | Lifetime total distance ridden |
108+
| Sensor | Unit | Example | Description |
109+
| --- | --- | --- | --- |
110+
| `career_level` || `25` | Current career level |
111+
| `total_xp` || `9,500` | Total experience points |
112+
| `total_coins` || `3,200` | Total coins earned |
113+
| `career_total_distance` | km | `4,567.8` | Lifetime total distance ridden |
114+
| `career_total_elevation` | m | `45,678` | Lifetime total elevation gained |
115+
| `career_total_time` | h | `312.5` | Lifetime total ride time |
116+
| `career_total_activities` || `247` | Lifetime total activity count |
117+
| `career_achievements` || `37` | Total achievements unlocked |
118+
| `career_trophies` || `12` | Total trophies earned |
111119

112120
### Social
113121

114-
| Sensor | Unit | Description |
115-
| --- | --- | --- |
116-
| `friends_count` || Total number of friends |
117-
| `friends_online` || Number of friends currently online |
122+
| Sensor | Unit | Example | Description |
123+
| --- | --- | --- | --- |
124+
| `friends_count` || `42` | Total number of friends |
125+
| `friends_online` || `5` | Number of friends currently online |
118126

119127
## Services
120128

121-
| Service | Description |
122-
| --- | --- |
123-
| `rouvy.update_weight` | Update body weight (kg) in Rouvy |
124-
| `rouvy.update_height` | Update height (cm) in Rouvy |
125-
| `rouvy.update_settings` | Update arbitrary profile settings (key-value pairs) |
129+
| Service | Parameters | Description |
130+
| --- | --- | --- |
131+
| `rouvy.update_weight` | `weight` (kg) | Update body weight in Rouvy |
132+
| `rouvy.update_height` | `height` (cm) | Update height in Rouvy |
133+
| `rouvy.update_settings` | `settings` (object) | Update arbitrary profile settings |
134+
| `rouvy.register_challenge` | `slug` | Register for a challenge |
135+
| `rouvy.register_event` | `event_id` (UUID) | Register for an event |
136+
| `rouvy.unregister_event` | `event_id` (UUID) | Unregister from an event |
126137

127138
## Logging
128139

@@ -173,7 +184,7 @@ uv run rouvy-api raw user-settings/zones.data # Raw decoded response
173184

174185
```bash
175186
uv sync # Install all dependencies
176-
uv run pytest -q # Run tests (438 tests)
187+
uv run pytest -q # Run tests (457 tests)
177188
uv run ruff check . # Lint
178189
uv run ruff format . # Format
179190
```
@@ -187,7 +198,7 @@ custom_components/rouvy/ # HA integration (HACS root)
187198
│ ├── parser.py # Turbo-stream response decoder
188199
│ └── ...
189200
├── coordinator.py # DataUpdateCoordinator
190-
├── sensor.py # 33 sensor descriptions
201+
├── sensor.py # 41 sensor descriptions
191202
├── config_flow.py # HA config flow + reauth
192203
├── services.yaml # Service definitions
193204
└── manifest.json # HA integration manifest

custom_components/rouvy/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,42 @@ async def _handle_update_settings(call: Any) -> None:
9494
await client.async_update_user_settings(settings)
9595
await entry.runtime_data.coordinator.async_request_refresh()
9696

97+
async def _handle_register_challenge(call: Any) -> None:
98+
slug = call.data["slug"]
99+
_LOGGER.info("Service call: register_challenge for %s", slug)
100+
for entry in hass.config_entries.async_entries(DOMAIN):
101+
if hasattr(entry, "runtime_data") and entry.runtime_data:
102+
client = entry.runtime_data.client
103+
await client.async_register_challenge(slug)
104+
await entry.runtime_data.coordinator.async_request_refresh()
105+
106+
async def _handle_register_event(call: Any) -> None:
107+
event_id = call.data["event_id"]
108+
_LOGGER.info("Service call: register_event for %s", event_id)
109+
for entry in hass.config_entries.async_entries(DOMAIN):
110+
if hasattr(entry, "runtime_data") and entry.runtime_data:
111+
client = entry.runtime_data.client
112+
await client.async_register_event(event_id)
113+
await entry.runtime_data.coordinator.async_request_refresh()
114+
115+
async def _handle_unregister_event(call: Any) -> None:
116+
event_id = call.data["event_id"]
117+
_LOGGER.info("Service call: unregister_event for %s", event_id)
118+
for entry in hass.config_entries.async_entries(DOMAIN):
119+
if hasattr(entry, "runtime_data") and entry.runtime_data:
120+
client = entry.runtime_data.client
121+
await client.async_unregister_event(event_id)
122+
await entry.runtime_data.coordinator.async_request_refresh()
123+
97124
if not hass.services.has_service(DOMAIN, "update_weight"):
98125
hass.services.async_register(DOMAIN, "update_weight", _handle_update_weight)
99126
if not hass.services.has_service(DOMAIN, "update_height"):
100127
hass.services.async_register(DOMAIN, "update_height", _handle_update_height)
101128
if not hass.services.has_service(DOMAIN, "update_settings"):
102129
hass.services.async_register(DOMAIN, "update_settings", _handle_update_settings)
130+
if not hass.services.has_service(DOMAIN, "register_challenge"):
131+
hass.services.async_register(DOMAIN, "register_challenge", _handle_register_challenge)
132+
if not hass.services.has_service(DOMAIN, "register_event"):
133+
hass.services.async_register(DOMAIN, "register_event", _handle_register_event)
134+
if not hass.services.has_service(DOMAIN, "unregister_event"):
135+
hass.services.async_register(DOMAIN, "unregister_event", _handle_unregister_event)

custom_components/rouvy/sensor.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ class RouvySensorDescription(SensorEntityDescription):
109109
f"{d.profile.first_name} {d.profile.last_name}".strip() or d.profile.username or None
110110
),
111111
),
112+
RouvySensorDescription(
113+
key="ftp_source",
114+
translation_key="ftp_source",
115+
value_fn=lambda d: d.profile.ftp_source or None,
116+
),
117+
RouvySensorDescription(
118+
key="country",
119+
translation_key="country",
120+
value_fn=lambda d: d.profile.country,
121+
),
112122
# Weekly activity stats sensors (current week ride totals)
113123
RouvySensorDescription(
114124
key="weekly_distance",
@@ -272,6 +282,11 @@ class RouvySensorDescription(SensorEntityDescription):
272282
translation_key="next_event",
273283
value_fn=lambda d: d.upcoming_events[0].title if d.upcoming_events else None,
274284
),
285+
RouvySensorDescription(
286+
key="next_event_time",
287+
translation_key="next_event_time",
288+
value_fn=lambda d: d.upcoming_events[0].start_date_time if d.upcoming_events else None,
289+
),
275290
# Career sensors
276291
RouvySensorDescription(
277292
key="career_level",
@@ -304,6 +319,44 @@ class RouvySensorDescription(SensorEntityDescription):
304319
else None
305320
),
306321
),
322+
RouvySensorDescription(
323+
key="career_total_elevation",
324+
translation_key="career_total_elevation",
325+
native_unit_of_measurement=UnitOfLength.METERS,
326+
device_class=SensorDeviceClass.DISTANCE,
327+
state_class=SensorStateClass.TOTAL,
328+
suggested_display_precision=0,
329+
value_fn=lambda d: round(d.career.total_elevation_m) if d.career else None,
330+
),
331+
RouvySensorDescription(
332+
key="career_total_time",
333+
translation_key="career_total_time",
334+
native_unit_of_measurement=UnitOfTime.HOURS,
335+
device_class=SensorDeviceClass.DURATION,
336+
state_class=SensorStateClass.TOTAL,
337+
suggested_display_precision=1,
338+
value_fn=lambda d: (
339+
round(d.career.total_time_seconds / 3600, 1) if d.career else None
340+
),
341+
),
342+
RouvySensorDescription(
343+
key="career_total_activities",
344+
translation_key="career_total_activities",
345+
state_class=SensorStateClass.TOTAL,
346+
value_fn=lambda d: d.career.total_activities if d.career else None,
347+
),
348+
RouvySensorDescription(
349+
key="career_achievements",
350+
translation_key="career_achievements",
351+
state_class=SensorStateClass.TOTAL,
352+
value_fn=lambda d: d.career.total_achievements if d.career else None,
353+
),
354+
RouvySensorDescription(
355+
key="career_trophies",
356+
translation_key="career_trophies",
357+
state_class=SensorStateClass.TOTAL,
358+
value_fn=lambda d: d.career.total_trophies if d.career else None,
359+
),
307360
# Friends sensors
308361
RouvySensorDescription(
309362
key="friends_count",

custom_components/rouvy/services.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,39 @@ update_settings:
4141
example: '{"weight": 80, "height": 178}'
4242
selector:
4343
object:
44+
45+
register_challenge:
46+
name: Register Challenge
47+
description: Register for a Rouvy challenge by its slug.
48+
fields:
49+
slug:
50+
name: Slug
51+
description: The challenge slug identifier (e.g., "april-2026-challenge").
52+
required: true
53+
example: "april-2026-challenge"
54+
selector:
55+
text:
56+
57+
register_event:
58+
name: Register Event
59+
description: Register for a Rouvy event by its ID.
60+
fields:
61+
event_id:
62+
name: Event ID
63+
description: The UUID of the event to register for.
64+
required: true
65+
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
66+
selector:
67+
text:
68+
69+
unregister_event:
70+
name: Unregister Event
71+
description: Unregister from a Rouvy event by its ID.
72+
fields:
73+
event_id:
74+
name: Event ID
75+
description: The UUID of the event to unregister from.
76+
required: true
77+
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
78+
selector:
79+
text:

custom_components/rouvy/strings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
"max_heart_rate": { "name": "Max Heart Rate" },
3737
"units": { "name": "Units" },
3838
"name": { "name": "Name" },
39+
"ftp_source": { "name": "FTP Source" },
40+
"country": { "name": "Country" },
3941
"weekly_distance": { "name": "Weekly Distance" },
4042
"weekly_elevation": { "name": "Weekly Elevation" },
4143
"weekly_calories": { "name": "Weekly Calories" },
@@ -57,10 +59,16 @@
5759
"routes_online_riders": { "name": "Online Riders (Favorites)" },
5860
"upcoming_events_count": { "name": "Upcoming Events" },
5961
"next_event": { "name": "Next Event" },
62+
"next_event_time": { "name": "Next Event Time" },
6063
"career_level": { "name": "Career Level" },
6164
"total_xp": { "name": "Total XP" },
6265
"total_coins": { "name": "Total Coins" },
6366
"career_total_distance": { "name": "Career Total Distance" },
67+
"career_total_elevation": { "name": "Career Total Elevation" },
68+
"career_total_time": { "name": "Career Total Time" },
69+
"career_total_activities": { "name": "Career Total Activities" },
70+
"career_achievements": { "name": "Career Achievements" },
71+
"career_trophies": { "name": "Career Trophies" },
6472
"friends_count": { "name": "Friends" },
6573
"friends_online": { "name": "Online Friends" }
6674
}

0 commit comments

Comments
 (0)