diff --git a/CHANGELOG.md b/CHANGELOG.md index fab468c..bde798d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to MMM-MessageCenter are documented in this file. +## 0.8.1 - 2026-10-11 + +### Changed + +- Put the leading team and higher score first in numeric head-to-head halftime, + score, and final messages, with visible lead, tie, and final indicators. +- Preserve provider ordering for non-numeric and ranked results instead of + guessing at sport-specific standings. + ## 0.8.0 - 2026-10-10 ### Added diff --git a/README.md b/README.md index fcdb33d..4e3566c 100644 --- a/README.md +++ b/README.md @@ -471,6 +471,12 @@ Use MyScoreboard's `leagueEvents` overrides to opt low-scoring leagues into MLS goals are included while NBA games continue to use the quieter global start, halftime, and final list. +For numeric head-to-head scores, MessageCenter puts the leading team and higher +score first with a visible lead, tie, or final marker. Non-numeric scores and +ranked events retain the provider's original order rather than applying +head-to-head assumptions; the source module remains responsible for standings, +golf, racing, and similar sport-specific meaning. + The source module remains responsible for selecting teams, retrieving scores, and deciding when a game reaches each milestone. MessageCenter only validates, normalizes, and presents the event. Either module continues to operate normally diff --git a/integrations/my-scoreboard.js b/integrations/my-scoreboard.js index 889bc6d..333acb9 100644 --- a/integrations/my-scoreboard.js +++ b/integrations/my-scoreboard.js @@ -43,6 +43,44 @@ return null; } + function numericScore(value) { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value !== "string" || !/^-?\d+(?:\.\d+)?$/.test(value.trim())) return null; + const number = Number(value); + return Number.isFinite(number) ? number : null; + } + + function scoreBody(module, event, away, awayScore, home, homeScore, fallbackKey) { + if (!["game.halftime", "game.score", "game.final"].includes(event)) { + return module.translate(fallbackKey, { away, awayScore, home, homeScore }); + } + + const awayNumber = numericScore(awayScore); + const homeNumber = numericScore(homeScore); + if (awayNumber === null || homeNumber === null) { + return module.translate(fallbackKey, { away, awayScore, home, homeScore }); + } + + if (awayNumber === homeNumber) { + const key = event === "game.final" ? "SPORTS_FINAL_TIED" : event === "game.halftime" + ? "SPORTS_HALFTIME_TIED" + : "SPORTS_SCORE_TIED"; + return module.translate(key, { away, awayScore, home, homeScore }); + } + + const awayLeads = awayNumber > homeNumber; + const values = { + leader: awayLeads ? away : home, + leaderScore: awayLeads ? awayScore : homeScore, + trailer: awayLeads ? home : away, + trailerScore: awayLeads ? homeScore : awayScore + }; + const key = event === "game.final" ? "SPORTS_FINAL_WINNER" : event === "game.halftime" + ? "SPORTS_HALFTIME_LEADER" + : "SPORTS_SCORE_LEADER"; + return module.translate(key, values); + } + root.MessageCenterAdapters.register({ id: "my-scoreboard", priority: 800, @@ -76,7 +114,7 @@ source, entityId: payload.gameId.trim(), title: module.translate("SPORTS_MATCHUP", { away, home }), - body: module.translate(eventConfig.bodyKey, { away, awayScore, home, homeScore }), + body: scoreBody(module, payload.event, away, awayScore, home, homeScore, eventConfig.bodyKey), urgency: eventConfig.urgency, retention: eventConfig.retention, timestamp, diff --git a/package-lock.json b/package-lock.json index 8d9d2e0..e9e6b01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mmm-messagecenter", - "version": "0.8.0", + "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mmm-messagecenter", - "version": "0.8.0", + "version": "0.8.1", "license": "MIT", "dependencies": { "express": "^5.2.1", diff --git a/package.json b/package.json index f7e1896..f161af1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmm-messagecenter", - "version": "0.8.0", + "version": "0.8.1", "description": "Hardware-independent notification and attention hub for MagicMirror²", "private": true, "type": "commonjs", diff --git a/test/MMM-MessageCenter.test.js b/test/MMM-MessageCenter.test.js index 7663991..ae0520f 100644 --- a/test/MMM-MessageCenter.test.js +++ b/test/MMM-MessageCenter.test.js @@ -1334,14 +1334,14 @@ test("normalizes MyScoreboard start, halftime, and final milestones with calm li id: "NBA:MIA@ORL:game.final", type: "sports.game.final", title: "Miami Heat at Orlando Magic", - body: "Final. Miami Heat 101, Orlando Magic 108.", + body: "✓ Orlando Magic 108 · Miami Heat 101 — Final", urgency: "attention" }, { id: "NBA:MIA@ORL:game.halftime", type: "sports.game.halftime", title: "Miami Heat at Orlando Magic", - body: "Halftime. Miami Heat 49, Orlando Magic 55.", + body: "▲ Orlando Magic 55 · Miami Heat 49 — Leads at halftime", urgency: "attention" }, { @@ -1382,16 +1382,42 @@ test("normalizes distinct opt-in MyScoreboard score events", () => { assert.deepEqual(module.messages.map(({ id, body }) => ({ id, body })), [ { id: "USA_MLS:CLB@ORL:game.score:0-2", - body: "Score update. Columbus Crew 0, Orlando City SC 2." + body: "▲ Orlando City SC 2 · Columbus Crew 0 — Leads" }, { id: "USA_MLS:CLB@ORL:game.score:0-1", - body: "Score update. Columbus Crew 0, Orlando City SC 1." + body: "▲ Orlando City SC 1 · Columbus Crew 0 — Leads" } ]); assert.equal(module.messages[0].expires, base.timestamp + 60000 + 2 * 60 * 60000); }); +test("makes tied head-to-head scores clear at a glance", () => { + const module = instance(); + + module.notificationReceived("MYSCOREBOARD_GAME_EVENT", { + event: "game.score", + gameId: "USA_MLS:CLB@ORL", + away: { name: "Columbus Crew", score: "1" }, + home: { name: "Orlando City SC", score: "1" } + }, { name: "MMM-MyScoreboard" }); + + assert.equal(module.messages[0].body, "= Columbus Crew 1 · Orlando City SC 1 — Tied"); +}); + +test("preserves the provider order when scores are not directly comparable", () => { + const module = instance(); + + module.notificationReceived("MYSCOREBOARD_GAME_EVENT", { + event: "game.final", + gameId: "ranked:event", + away: { name: "Driver One", score: "P1" }, + home: { name: "Driver Two", score: "P2" } + }, { name: "MMM-MyScoreboard" }); + + assert.equal(module.messages[0].body, "Final. Driver One P1, Driver Two P2."); +}); + test("allows MyScoreboard event lifetimes to be configured", () => { const module = instance({ internalNotifications: { @@ -1432,7 +1458,7 @@ test("updates a duplicate MyScoreboard milestone silently and preserves read sta ); assert.equal(module.messages.length, 1); - assert.equal(module.messages[0].body, "Halftime. Miami Heat 49, Orlando Magic 56."); + assert.equal(module.messages[0].body, "▲ Orlando Magic 56 · Miami Heat 49 — Leads at halftime"); assert.equal(module.messages[0].unread, false); assert.equal(module.notifications.some(({ name }) => name === "SHOW_ALERT"), false); }); diff --git a/translations/de.json b/translations/de.json index 3335270..3f98545 100644 --- a/translations/de.json +++ b/translations/de.json @@ -29,6 +29,12 @@ "SPORTS_GAME_HALFTIME": "Halbzeit. {away} {awayScore}, {home} {homeScore}.", "SPORTS_GAME_SCORE": "Spielstand. {away} {awayScore}, {home} {homeScore}.", "SPORTS_GAME_FINAL": "Endstand. {away} {awayScore}, {home} {homeScore}.", + "SPORTS_HALFTIME_LEADER": "▲ {leader} {leaderScore} · {trailer} {trailerScore} — Führt zur Halbzeit", + "SPORTS_HALFTIME_TIED": "= {away} {awayScore} · {home} {homeScore} — Remis zur Halbzeit", + "SPORTS_SCORE_LEADER": "▲ {leader} {leaderScore} · {trailer} {trailerScore} — Führt", + "SPORTS_SCORE_TIED": "= {away} {awayScore} · {home} {homeScore} — Remis", + "SPORTS_FINAL_WINNER": "✓ {leader} {leaderScore} · {trailer} {trailerScore} — Endstand", + "SPORTS_FINAL_TIED": "= {away} {awayScore} · {home} {homeScore} — Endstand", "SOURCE_HOME_ASSISTANT": "Home Assistant", "SOURCE_SMARTTHINGS_HOME_ASSISTANT": "SmartThings über Home Assistant", "SOURCE_SMARTTHINGS": "SmartThings" diff --git a/translations/en.json b/translations/en.json index 0c7320a..ee1b3dd 100644 --- a/translations/en.json +++ b/translations/en.json @@ -29,6 +29,12 @@ "SPORTS_GAME_HALFTIME": "Halftime. {away} {awayScore}, {home} {homeScore}.", "SPORTS_GAME_SCORE": "Score update. {away} {awayScore}, {home} {homeScore}.", "SPORTS_GAME_FINAL": "Final. {away} {awayScore}, {home} {homeScore}.", + "SPORTS_HALFTIME_LEADER": "▲ {leader} {leaderScore} · {trailer} {trailerScore} — Leads at halftime", + "SPORTS_HALFTIME_TIED": "= {away} {awayScore} · {home} {homeScore} — Tied at halftime", + "SPORTS_SCORE_LEADER": "▲ {leader} {leaderScore} · {trailer} {trailerScore} — Leads", + "SPORTS_SCORE_TIED": "= {away} {awayScore} · {home} {homeScore} — Tied", + "SPORTS_FINAL_WINNER": "✓ {leader} {leaderScore} · {trailer} {trailerScore} — Final", + "SPORTS_FINAL_TIED": "= {away} {awayScore} · {home} {homeScore} — Final", "SOURCE_HOME_ASSISTANT": "Home Assistant", "SOURCE_SMARTTHINGS_HOME_ASSISTANT": "SmartThings via Home Assistant", "SOURCE_SMARTTHINGS": "SmartThings" diff --git a/translations/es.json b/translations/es.json index 122b507..9e44263 100644 --- a/translations/es.json +++ b/translations/es.json @@ -29,6 +29,12 @@ "SPORTS_GAME_HALFTIME": "Descanso. {away} {awayScore}, {home} {homeScore}.", "SPORTS_GAME_SCORE": "Marcador. {away} {awayScore}, {home} {homeScore}.", "SPORTS_GAME_FINAL": "Final. {away} {awayScore}, {home} {homeScore}.", + "SPORTS_HALFTIME_LEADER": "▲ {leader} {leaderScore} · {trailer} {trailerScore} — Lidera al descanso", + "SPORTS_HALFTIME_TIED": "= {away} {awayScore} · {home} {homeScore} — Empate al descanso", + "SPORTS_SCORE_LEADER": "▲ {leader} {leaderScore} · {trailer} {trailerScore} — Lidera", + "SPORTS_SCORE_TIED": "= {away} {awayScore} · {home} {homeScore} — Empate", + "SPORTS_FINAL_WINNER": "✓ {leader} {leaderScore} · {trailer} {trailerScore} — Final", + "SPORTS_FINAL_TIED": "= {away} {awayScore} · {home} {homeScore} — Final", "SOURCE_HOME_ASSISTANT": "Home Assistant", "SOURCE_SMARTTHINGS_HOME_ASSISTANT": "SmartThings mediante Home Assistant", "SOURCE_SMARTTHINGS": "SmartThings" diff --git a/translations/fr.json b/translations/fr.json index 7f9ecc6..08f45df 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -29,6 +29,12 @@ "SPORTS_GAME_HALFTIME": "Mi-temps. {away} {awayScore}, {home} {homeScore}.", "SPORTS_GAME_SCORE": "Mise à jour du score. {away} {awayScore}, {home} {homeScore}.", "SPORTS_GAME_FINAL": "Score final. {away} {awayScore}, {home} {homeScore}.", + "SPORTS_HALFTIME_LEADER": "▲ {leader} {leaderScore} · {trailer} {trailerScore} — Mène à la mi-temps", + "SPORTS_HALFTIME_TIED": "= {away} {awayScore} · {home} {homeScore} — Égalité à la mi-temps", + "SPORTS_SCORE_LEADER": "▲ {leader} {leaderScore} · {trailer} {trailerScore} — Mène", + "SPORTS_SCORE_TIED": "= {away} {awayScore} · {home} {homeScore} — Égalité", + "SPORTS_FINAL_WINNER": "✓ {leader} {leaderScore} · {trailer} {trailerScore} — Score final", + "SPORTS_FINAL_TIED": "= {away} {awayScore} · {home} {homeScore} — Score final", "SOURCE_HOME_ASSISTANT": "Home Assistant", "SOURCE_SMARTTHINGS_HOME_ASSISTANT": "SmartThings via Home Assistant", "SOURCE_SMARTTHINGS": "SmartThings"