Skip to content

Commit 2c34500

Browse files
authored
Merge pull request #149 from Nicxe/beta
v.2.3.3
2 parents fd92982 + 36bdbbb commit 2c34500

File tree

16 files changed

+176
-566
lines changed

16 files changed

+176
-566
lines changed

README.md

Lines changed: 15 additions & 496 deletions
Large diffs are not rendered by default.

custom_components/f1_sensor/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,28 @@ def _extract_items(msg) -> list[dict]:
359359
return [m for m in msg if isinstance(m, dict)]
360360
if isinstance(msg, dict):
361361
# Some payloads contain { "Messages": [ ... ] }
362-
if isinstance(msg.get("Messages"), list):
363-
return [m for m in msg.get("Messages") if isinstance(m, dict)]
362+
messages = msg.get("Messages")
363+
if isinstance(messages, list):
364+
return [m for m in messages if isinstance(m, dict)]
365+
# Some payloads contain { "Messages": { "1": {...}, "2": {...}, ... } }
366+
if isinstance(messages, dict) and messages:
367+
try:
368+
numeric_keys = [k for k in messages.keys() if str(k).isdigit()]
369+
# Sort by numeric key to preserve order if automations iterate
370+
numeric_keys.sort(key=lambda x: int(x))
371+
result: list[dict] = []
372+
for key in numeric_keys:
373+
val = messages.get(key)
374+
if isinstance(val, dict):
375+
item = dict(val)
376+
# Provide stable id if not present
377+
item.setdefault("id", int(key))
378+
result.append(item)
379+
if result:
380+
return result
381+
except Exception:
382+
# Fall through to wrapper-as-item if something unexpected happens
383+
pass
364384
# Or a single message
365385
return [msg]
366386
return []

custom_components/f1_sensor/helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ def normalize_track_status(raw: dict | None) -> str | None:
6262
"4": "SC", # Säkrast stöd för Safety Car
6363
"5": "RED",
6464
"6": "VSC",
65-
"7": "VSC_ENDING",
65+
# Code "7" represents VSC ending phase; map to canonical VSC
66+
"7": "VSC",
6667
"8": "CLEAR", # Fallback, observerad som CLEAR i praktiken
6768
# "3": okänd/kontextberoende – logga och validera mot Race Control
6869
}

custom_components/f1_sensor/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"iot_class": "cloud_polling",
88
"issue_tracker": "https://github.com/Nicxe/f1_sensor/issues",
99
"requirements": ["timezonefinder==5.2.0", "python-dateutil>=2.8"],
10-
"version": "2.3.2"
10+
"version": "2.3.3"
1111
}

custom_components/f1_sensor/sensor.py

Lines changed: 59 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -458,36 +458,41 @@ async def _update_weather(self):
458458
if not times:
459459
return
460460
curr = times[0].get("data", {}).get("instant", {}).get("details", {})
461-
# Derive current precipitation from forecast block (instant does not include precipitation)
462-
current_forecast_block = (
463-
times[0].get("data", {}).get("next_1_hours")
464-
or times[0].get("data", {}).get("next_6_hours")
465-
or times[0].get("data", {}).get("next_12_hours")
466-
or {}
467-
)
468-
current_precip_details = current_forecast_block.get("details", {})
469-
current_precip = (
470-
current_precip_details.get("precipitation_amount")
471-
if current_precip_details.get("precipitation_amount") is not None
472-
else (
473-
current_precip_details.get("precipitation_amount_min")
474-
if current_precip_details.get("precipitation_amount_min") is not None
475-
else current_precip_details.get("precipitation_amount_max", 0)
476-
)
477-
)
461+
# Derive current precipitation from forecast blocks (prefer max of 1h/6h/12h)
462+
data0 = times[0].get("data", {})
463+
block1 = data0.get("next_1_hours") or {}
464+
block6 = data0.get("next_6_hours") or {}
465+
block12 = data0.get("next_12_hours") or {}
466+
467+
def _precip_triplet(block: dict):
468+
details = (block or {}).get("details", {}) or {}
469+
amt = details.get("precipitation_amount")
470+
if amt is None:
471+
amt = details.get("precipitation_amount_min")
472+
if amt is None:
473+
amt = details.get("precipitation_amount_max")
474+
if amt is None:
475+
amt = 0
476+
return amt, details.get("precipitation_amount_min"), details.get("precipitation_amount_max")
477+
478+
p1, p1min, p1max = _precip_triplet(block1)
479+
p6, p6min, p6max = _precip_triplet(block6)
480+
p12, p12min, p12max = _precip_triplet(block12)
481+
candidates = [
482+
(p1, p1min, p1max, block1),
483+
(p6, p6min, p6max, block6),
484+
(p12, p12min, p12max, block12),
485+
]
486+
sel_amt, sel_min, sel_max, sel_block = max(candidates, key=lambda t: t[0])
478487
curr_with_precip = dict(curr)
479-
curr_with_precip["precipitation_amount"] = current_precip
480-
# Also expose min/max precipitation from forecast if available; fallback to amount
481-
_cur_min = current_precip_details.get("precipitation_amount_min")
482-
_cur_max = current_precip_details.get("precipitation_amount_max")
483-
curr_with_precip["precipitation_amount_min"] = _cur_min if _cur_min is not None else current_precip
484-
curr_with_precip["precipitation_amount_max"] = _cur_max if _cur_max is not None else current_precip
488+
curr_with_precip["precipitation_amount"] = sel_amt
489+
# Also expose min/max precipitation; fallback to selected amount when missing
490+
_cur_min = sel_min
491+
_cur_max = sel_max
492+
curr_with_precip["precipitation_amount_min"] = _cur_min if _cur_min is not None else sel_amt
493+
curr_with_precip["precipitation_amount_max"] = _cur_max if _cur_max is not None else sel_amt
485494
self._current = self._extract(curr_with_precip)
486-
current_symbol = (
487-
current_forecast_block
488-
.get("summary", {})
489-
.get("symbol_code")
490-
)
495+
current_symbol = (sel_block or {}).get("summary", {}).get("symbol_code")
491496
current_icon = SYMBOL_CODE_TO_MDI.get(current_symbol, self._attr_icon)
492497
self._attr_icon = current_icon
493498
start_iso = (
@@ -512,36 +517,26 @@ async def _update_weather(self):
512517
)
513518
data_entry = closest.get("data", {})
514519
instant_details = data_entry.get("instant", {}).get("details", {})
515-
# Prefer 1-hour precipitation at race start, fallback to 6/12-hour blocks
516-
race_forecast_block = (
517-
data_entry.get("next_1_hours")
518-
or data_entry.get("next_6_hours")
519-
or data_entry.get("next_12_hours")
520-
or {}
521-
)
522-
race_precip_details = race_forecast_block.get("details", {})
523-
race_precip = (
524-
race_precip_details.get("precipitation_amount")
525-
if race_precip_details.get("precipitation_amount") is not None
526-
else (
527-
race_precip_details.get("precipitation_amount_min")
528-
if race_precip_details.get("precipitation_amount_min") is not None
529-
else race_precip_details.get("precipitation_amount_max", 0)
530-
)
531-
)
520+
# Choose race precipitation as max of 1h/6h/12h around start time
521+
r1 = data_entry.get("next_1_hours") or {}
522+
r6 = data_entry.get("next_6_hours") or {}
523+
r12 = data_entry.get("next_12_hours") or {}
524+
rp1, rp1min, rp1max = _precip_triplet(r1)
525+
rp6, rp6min, rp6max = _precip_triplet(r6)
526+
rp12, rp12min, rp12max = _precip_triplet(r12)
527+
rcandidates = [
528+
(rp1, rp1min, rp1max, r1),
529+
(rp6, rp6min, rp6max, r6),
530+
(rp12, rp12min, rp12max, r12),
531+
]
532+
r_sel_amt, r_sel_min, r_sel_max, r_sel_block = max(rcandidates, key=lambda t: t[0])
532533
rd = dict(instant_details)
533-
rd["precipitation_amount"] = race_precip
534-
# Also expose min/max precipitation at race time if available; fallback to amount
535-
_race_min = race_precip_details.get("precipitation_amount_min")
536-
_race_max = race_precip_details.get("precipitation_amount_max")
537-
rd["precipitation_amount_min"] = _race_min if _race_min is not None else race_precip
538-
rd["precipitation_amount_max"] = _race_max if _race_max is not None else race_precip
534+
rd["precipitation_amount"] = r_sel_amt
535+
# Also expose min/max precipitation at race time; fallback to selected amount
536+
rd["precipitation_amount_min"] = r_sel_min if r_sel_min is not None else r_sel_amt
537+
rd["precipitation_amount_max"] = r_sel_max if r_sel_max is not None else r_sel_amt
539538
self._race = self._extract(rd)
540-
forecast_block = (
541-
data_entry.get("next_1_hours")
542-
or data_entry.get("next_6_hours")
543-
or data_entry.get("next_12_hours", {})
544-
)
539+
forecast_block = r_sel_block or {}
545540
race_symbol = forecast_block.get("summary", {}).get("symbol_code")
546541
race_icon = SYMBOL_CODE_TO_MDI.get(race_symbol, self._attr_icon)
547542
self._race["weather_icon"] = race_icon
@@ -1537,7 +1532,6 @@ def __init__(self, coordinator, sensor_name, unique_id, entry_id, device_name):
15371532
"CLEAR",
15381533
"YELLOW",
15391534
"VSC",
1540-
"VSC_ENDING",
15411535
"SC",
15421536
"RED",
15431537
]
@@ -2175,6 +2169,14 @@ def _apply_payload(self, raw: dict) -> None:
21752169

21762170
self._attr_native_value = curr
21772171
self._last_received_utc = now_utc
2172+
# Preserve last known total_laps if not present in this payload to avoid transient 'unknown'
2173+
prev_total = None
2174+
try:
2175+
prev_total = (self._attr_extra_state_attributes or {}).get("total_laps")
2176+
except Exception:
2177+
prev_total = None
2178+
if total is None:
2179+
total = prev_total
21782180
self._attr_extra_state_attributes = {
21792181
"total_laps": total,
21802182
}

docs/automation.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
id: automation
3+
title: Automation
4+
---
5+
6+
7+
### Synchronize your lights with the flag status
8+
9+
The Formula 1 Track Status Blueprint for Home Assistant lets you synchronize your lights with the live race flag status.
10+
11+
[![Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.](https://my.home-assistant.io/badges/blueprint_import.svg)](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2FEvertJob%2FF1-Blueprint%2Fblob%2Fmain%2Fblueprint%2Ff1.yaml)
12+
13+
![F1SensorFlag-ezgif com-video-to-gif-converter (5)](https://github.com/user-attachments/assets/18a74679-76e2-4d10-8a0d-d3f111c42593)
14+
15+
*It is now maintained by [EvertJob](https://github.com/EvertJob/). You can find the latest version and full setup instructions here: 👉 [F1-Blueprint](https://github.com/EvertJob/F1-Blueprint)*
16+
17+
---
18+
19+
### Race Control Event Notifications
20+
21+
This automation listens for f1_sensor_race_control_event events and sends the Message field from each event as a notification in Home Assistant. It provides real-time updates from Race Control, such as flag changes or incident reports.
22+
23+
```yaml
24+
alias: F1 - Race Control Notification
25+
description: Sends Race Control messages as notifications in Home Assistant
26+
trigger:
27+
- platform: event
28+
event_type: f1_sensor_race_control_event
29+
condition: []
30+
action:
31+
- service: notify.persistent_notification
32+
data:
33+
title: "Race Control"
34+
message: "{{ trigger.event.data.message.Message }}"
35+
mode: queued
36+
max: 10
37+
```

docs/entities/events.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
id: events
33
title: Race Control
4-
description: To add the integration to your Home Assistant instance
54
---
65

76

docs/entities/live_data.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
id: live-data
33
title: Live Data
4-
description: To add the integration to your Home Assistant instance
54
---
65

76
By enabling [live data](/getting-started/add-integration) when configuring the F1 Sensor, Home Assistant can react to live data from an ongoing session such as practice, qualifying, or race. These entities update shortly before, during, and shortly after a session. Outside session times, they will not update. This means that a sensor may show as unknown or display the last known state even when no session is active. For example, the Track Status entity often remains `CLEAR` between race weekends.

docs/entities/static_data.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
id: static-data
33
title: Static Data
4-
description: To add the integration to your Home Assistant instance
54
---
65

76
Information that rarely changes, such as schedules, drivers, circuits, and championship standings.

docs/example/e-ink.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
---
22
id: e-ink
33
title: E-ink Display
4-
description: Learn how to contribute to the project, and support the development of the F1 Sensor for Home Assistant
54
---
65

7-
I personally use this integration to display the next race and the following three races on an e-ink display. You can read more about that setup here.
6+
7+
I personally use this integration to display the next race and the following three races on an e-ink display.
8+
9+
You can read more about that setup here - [E-ink displays power by ESPhome and Home Assistant](https://github.com/Nicxe/esphome).
10+
11+
12+
13+
![E-ink display](/img/F1_pitwall.png)

0 commit comments

Comments
 (0)