fix for #48 as well#52
Conversation
Adds a brand/ directory to both the custom_components integration and the bundled addon copy, providing icon.png, logo.png and their dark-mode variants. The icon uses the official Raumfeld bars mark + wordmark; the logo banner combines the Teufel script wordmark with the Raumfeld logo, separated by a red accent bar. Fixes the missing-icon issue during integration setup in Home Assistant 2026.3+. Co-authored-by: Cursor <cursoragent@cursor.com>
Raumfeld devices have a quirk: when play() is called after PAUSED_PLAYBACK, the device restarts from the last seek anchor (the position set by the most recent SetAVTransportURI or Seek) rather than the actual paused position. This is most visible with Music Assistant playback where a track is loaded via URI and then seeked into -- pausing and resuming would jump back to the seek target instead of where playback was paused. Fix: in RaumkernelHelper.play(), if the renderer is in PAUSED_PLAYBACK state, issue a Seek to the current RelativeTimePosition before calling play(). This refreshes the anchor to the actual paused position so the device resumes from the correct spot. Bumps version to 1.2.14. Co-authored-by: Cursor <cursoragent@cursor.com>
to test cursors changes
The v1.2.14 fix (seek while paused to refresh the anchor) had two weak points that could silently prevent it from working: 1. It read RelativeTimePosition from the cached rendererState, which may not have been updated by a subscription event since the pause. Changed to call renderer.getPositionInfo() for a live query; falls back to rendererState.RelativeTimePosition on error. 2. It called renderer.seek() immediately followed by renderer.play() with no delay. The Raumfeld device appears to need a small window after processing the Seek before the anchor is committed; adding a 300 ms pause (matching the delay already used after seeks in RaumkernelHelper.seek()) ensures the device has updated its anchor before Play is issued. Also adds an explicit guard for 'NOT_IMPLEMENTED', which some UPnP devices return for RelTime when position tracking is unsupported, to avoid sending an invalid Seek target. Bumps version to 1.2.15. Co-authored-by: Cursor <cursoragent@cursor.com>
…h (v1.2.16)
v1.2.15 introduced a regression: getPositionInfo().RelTime turns out to
be the *seek anchor* on the Raumfeld zone renderer (it freezes at the
target of the most recent Seek and does not advance during playback).
After the first corrective seek the anchor became the first-pause
position; every subsequent resume then seeked back to that same spot.
Root cause: the device exposes no reliable way to read the advancing
playback position — GetPositionInfo.RelTime and rendererState
RelativeTimePosition both reflect the last explicit seek target, not
the live streaming clock.
Fix: use wall-clock elapsed time as the position source for all resumes
after the first one.
• First resume (anchor ≠ device RelTime, so getPositionInfo() still
returns the actual position): unchanged — query getPositionInfo().
• After the corrective seek succeeds: record the anchor in seconds
(_resumeAnchorSeconds) and the current wall-clock (_resumeAnchorTime)
on the room object.
• Every subsequent resume from PAUSED_PLAYBACK: compute
estimatedPos = _resumeAnchorSeconds + (now − _resumeAnchorTime)
This is the total time the track has been playing since the last
anchor update, giving a position independent of the device's broken
RelTime reporting.
• Reset the tracker on: user-initiated seek (new anchor from MA/user),
stop(), and any play() that is NOT a resume from pause.
Bumps version to 1.2.16.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Raumfeld zone renderer's RelativeTimePosition is frozen at the last
seek anchor and never advances during playback. This caused the HA
position display to reset to 0 whenever the device sent a PAUSE event
(because the event carried RelativeTimePosition = anchor = 0).
Fix: maintain a wall-clock elapsed-time tracker on each room object that
is independent of the device's own position reporting.
Tracker lifecycle:
• loadUri / loadContainer / loadSingle: reset to {0, now} when a new
track starts so position advances correctly from the very first pause.
• seek(): advance to {seekPos, now} instead of clearing, so the tracker
stays valid across user/MA-initiated seeks.
• play() resume-from-pause: after the corrective Seek, update the
tracker to {resumePos, now} (already done in v1.2.16).
• play() resume-from-pause: now uses the tracker directly (frozen value
= correct paused position) instead of requiring getPositionInfo().
• stop(): still clears the tracker (no meaningful position after stop).
Freeze / thaw (inside _extractNowPlaying, called on every state change):
• PAUSED/STOPPED: freeze — compute elapsed, add to _resumeAnchorSeconds,
set _resumeAnchorTime = null. This gives the exact paused position
without querying the device, and prevents drift during long pauses.
• PLAYING (timer frozen): thaw — restart the clock, so external play
commands (not through our play() path) are also handled.
_getPositionForRoom updated to return:
anchor + elapsed (timer running, device is playing)
anchor (timer frozen, device is paused)
defaultPosition (no tracking yet)
Bumps version to 1.2.17.
Co-authored-by: Cursor <cursoragent@cursor.com>
findRoom() was returning plain objects from _state.availableRooms, which are rebuilt from scratch on every _broadcastRoomStates() call. Any tracker state written to those plain objects (room._resumeAnchorSeconds, room._resumeAnchorTime, room._lastSeekPosition, room._lastSeekTime) was silently discarded on the next broadcast, so the position fix in v1.2.17 had no lasting effect and the display always fell back to 0. Fix: rewrite findRoom() to search this._rooms directly (the persistent rich room objects that carry all mutable per-room state) using the same UDN / name matching logic as before. Command handlers (play, pause, loadUri, seek, etc.) already call findRoom() so they now automatically get the object whose fields survive across broadcasts. Bumps version to 1.2.18. Co-authored-by: Cursor <cursoragent@cursor.com>
Move the elapsed-time tracker freeze from the transport-state change handler (_extractNowPlaying) into the pause() command handler. This avoids a race condition where a late subscription event from a corrective Seek (carrying TransportState=PAUSED_PLAYBACK) arrives after _resumeAnchorTime was set, which would trigger a spurious freeze and reset the position back to the first-pause value.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Hello Holli, thanks for your contribution! It seems the two PRs got mixed up. This PR seems to contain the other PR as well which you opened. Please see my comment there regarding the trademark situation. In addition, I saw you are changing the repo owner to yourself and pointing the installer to your repo in the repository.yaml file. I assume this is not intended :) When you are done with your changes, please add a description to the PR and review the AI generated code for any larger issues. For now, I will mark your PR as not ready to merge so that I can keep track of what needs attention. Just comment again when you want me to have a look. Best, Uli |
…2.20) Raumfeld devices don't support seeking on live streams (TuneIn, internet radio). Attempting a seek while paused on a live source causes the device to disconnect from the stream; it plays from whatever is buffered and then stops. Also skip the seek when the track URI changed externally (e.g. Music Assistant loaded a new track that bypassed our loadUri), because the elapsed-time tracker would be stale from the previous track. Guard logic added to play(): - Check CurrentTrackDuration: "0:00:00" or NOT_IMPLEMENTED = live stream, skip seek - Check CurrentTransportActions: Seek must be listed, otherwise skip - Check AVTransportURI vs stored _resumeAnchorUri: if changed externally, skip seek and invalidate the stale tracker Also store _resumeAnchorUri in loadUri() so external URI changes can be detected. Co-authored-by: Cursor <cursoragent@cursor.com>
Implements UPnP SetPlayMode for shuffle and repeat across all layers:
RaumkernelHelper.js:
- setShuffle(room, bool): maps to SHUFFLE/RANDOM/NORMAL/REPEAT_ALL preserving
the current repeat state
- setRepeat(room, 'off'|'one'|'all'): maps to NORMAL/REPEAT_ONE/REPEAT_ALL/RANDOM
preserving the current shuffle state
- _extractNowPlaying now includes shuffle (bool) and repeat ('off'|'one'|'all')
derived from renderer's CurrentPlayMode
index.js: setShuffle and setRepeat WebSocket command handlers
api.py: set_shuffle() and set_repeat() client methods
media_player.py:
- Imports RepeatMode
- Sets _attr_shuffle and _attr_repeat from state
- Adds SHUFFLE_SET and REPEAT_SET features when multi-track content is available
- Implements async_set_shuffle() and async_set_repeat()
Co-authored-by: Cursor <cursoragent@cursor.com>
SHUFFLE_SET and REPEAT_SET were gated on canPlayNext, so they were invisible when playing a single track or any content where the device doesn't report next-track capability. SetPlayMode is always available via UPnP so the features are now added unconditionally. Also initialise _attr_shuffle and _attr_repeat in __init__ so HA has safe defaults before the first real state update arrives. Co-authored-by: Cursor <cursoragent@cursor.com>
When the device auto-advances to the next track (playlist, Music Assistant queue) the AVTransportURI changes but our elapsed-time position tracker kept accumulating from the previous track, showing wrong times. Fix: detect URI changes in _extractNowPlaying. When AVTransportURI differs from the last recorded value the tracker is reset to 0 (with the clock started if the device is already PLAYING, frozen otherwise). Also re-adds the thaw logic (start the tracker clock when device transitions to PLAYING) that was removed in v1.2.19. This is now safe because only the thaw lives in _extractNowPlaying; the freeze remains exclusively in pause(). Our corrective Seek does not change the URI and does not change TransportState to PLAYING, so neither the URI check nor the thaw can be triggered spuriously by late subscription events. Co-authored-by: Cursor <cursoragent@cursor.com>
media_player.py: - Fix critical bug in _handle_event: was looking for payload.availableZones (always empty) instead of payload.availableRooms on fullStateUpdate events. This meant all renderer state changes (play mode, transport state, volume changes from fullStateUpdate) were silently ignored. shuffle/repeat values from the device were never received by HA entities. Play/pause appeared to work only because HA does optimistic UI updates on button press. RaumkernelHelper.js: - Replace URI-only track-change detection with a URI::CurrentTrack fingerprint. For container/playlist playback the AVTransportURI is a constant dlna-playcontainer:// reference; only CurrentTrack increments when the next track plays. The URI-only check never fired, so the position tracker kept accumulating from the previous track instead of resetting to 0. - loadUri/loadContainer/loadSingle: also reset _resumeAnchorTrack so the fingerprint is re-initialised cleanly on the next state event. - play(): update the "track changed externally" check to use the same URI::CurrentTrack fingerprint. Co-authored-by: Cursor <cursoragent@cursor.com>
ha-raumkernel-addon/teufel_raumfeld_raumkernel/{media_player.py,api.py}
were missing all changes from v1.2.21 through v1.2.24:
- shuffle/repeat support (SHUFFLE_SET, REPEAT_SET features, async_set_shuffle,
async_set_repeat, _attr_shuffle/_attr_repeat initialisation)
- fullStateUpdate handler bug fix (availableZones → availableRooms)
- set_shuffle / set_repeat API client methods
The addon auto-installs from ha-raumkernel-addon/teufel_raumfeld_raumkernel/
(mounted as /integration in Docker), not from custom_components/. All
edits to custom_components/ must be mirrored here; now in sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The previous sync commit kept version 1.2.24, so IntegrationManager would see installed==bundled and skip the auto-install, leaving the old files (without shuffle/repeat support) in HA. Bumping to 1.2.25 forces the auto-install to run and copy the corrected media_player.py and api.py. Also ran sync-integration.sh to bring the bundled copy fully up to date with custom_components/ (manifest.json version bump). Co-authored-by: Cursor <cursoragent@cursor.com>
….2.26)
The previous isLiveStream check only looked at CurrentTrackDuration. Some
TuneIn/internet-radio streams report a non-zero duration (the current song's
length on the station) and advertise 'Seek' in their transport actions. This
caused play() to issue a corrective seek on the live stream, disconnecting
the source; the device played from its buffer for a while and then stopped
("pauses on its own" symptom).
Fix: three independent live-stream guards in play() — any one is sufficient
to skip the seek:
1. Duration: missing / "0:00:00" / NOT_IMPLEMENTED (unchanged)
2. Media class: room._isLiveStream, set by _extractNowPlaying whenever
the UPnP class contains 'audioBroadcast' or 'radio'. Covers streams
loaded via loadUri/loadContainer/loadSingle.
3. Fresh metadata parse in play() itself: catches streams loaded
externally (Music Assistant, original Teufel app) that bypassed
loadUri() so room._isLiveStream was never set.
Also moves the isRadio calculation in _extractNowPlaying out of the
canPlayNext/canPlayPrev fallback block so it is always computed and can
be stored on the room object.
Co-authored-by: Cursor <cursoragent@cursor.com>
Root cause: when a radio station's "now playing" metadata updates, the UPnP object class changes from audioBroadcast → musicTrack for the current song. _extractNowPlaying was writing that false back to room._isLiveStream, wiping out the earlier live-stream detection. On the very next pause/resume the three guards all evaluated to false and play() issued a corrective seek on the live stream, which disconnects the source and causes it to stop. Fix: - _isLiveStream is now sticky: only ever set to true from _extractNowPlaying, never overwritten to false by a song-metadata update on the same URI. - Reset to undefined only when a genuinely new URI is loaded (loadUri / loadContainer / loadSingle) or when the AVTransportURI itself changes (detected in track-change logic), not when only CurrentTrack changes (which happens for song updates within the same radio stream). - loadUri / loadContainer / loadSingle each clear _isLiveStream = undefined so fresh detection always happens for new media. Bump to v1.2.27. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Problem: when a radio station's first state update from the device already shows the current song as musicTrack (not audioBroadcast), _isLiveStream is never set and the three existing guards all evaluate false simultaneously: • guard 1 (duration): non-zero because TuneIn reports the song length • guard 2 (class flag): undefined — audioBroadcast never seen • guard 3 (fresh metadata): also musicTrack A corrective seek then fires on the live stream, it plays from its buffer for a while, and then stops. Fix — add guard 4 (URI extension): Stream URLs (TuneIn, internet radio) have no audio file extension. NAS file URLs almost always end with .mp3 / .flac / .wav / etc. Any URI without a recognised audio extension is now treated as live and will not be seeked. Exception: dlna-playcontainer / dlna-playsingle URIs are finite-media containers and continue to receive corrective seeks. Also add a diagnostic console.log before the seek decision that prints all four guard values, the URI, and the duration string. This makes future stream-stop regressions diagnosable from the add-on log without code changes. Bump to v1.2.28. Co-authored-by: Cursor <cursoragent@cursor.com>
Root cause (confirmed from logs):
The <raumfeld:durability>120</raumfeld:durability> tag in TuneIn station
metadata is a Raumfeld session timer. After ~120 s the Raumfeld kernel
drops the stream because the TuneIn session URL has expired and no
controller renewed it. The Teufel app renews by calling SetAVTransportURI
again with the real station metadata (containing raumfeld:ebrowse URL).
Our add-on used renderer.loadUri() which only passes a generic template
without the ebrowse URL, so the kernel could never auto-renew.
Result: PLAYING → STOPPED after ≈90–120 s, no seek involved.
Fix:
• _parseMetadata now extracts raumfeld:ebrowse into result.ebrowseUrl.
• _extractNowPlaying caches room._radioMetaXml, room._radioEbrowseUrl, and
room._radioResUrl whenever the device reports real station metadata
(metadata that includes an ebrowse URL).
• When _extractNowPlaying detects a PLAYING → STOPPED/NO_MEDIA_PRESENT
transition on a live-stream room that was NOT user-initiated, it
schedules _autoRestartRadio() after a 2 s debounce.
• _autoRestartRadio() calls renderer.setAvTransportUri(ebrowseUrl, metaXml)
(the library's lower-level method that accepts custom metadata) so the
kernel receives the real ebrowse URL and can renew on the next cycle.
Falls back to renderer.loadUri() if no cached metadata is available.
• stop() sets room._userInitiatedStop = Date.now() to suppress the
auto-restart when the user explicitly stops playback.
Bump to v1.2.29.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The v1.2.29 auto-restart used the raumfeld:ebrowse URL as the SetAVTransportURI transport URI. That URL returns OPML XML (not an M3U stream), so the Raumfeld kernel tried to load it as a stream, failed, and got stuck in TRANSITIONING indefinitely. Fix: always use the <res> URL (M3U) as the transport URI; the real station metadata XML (including raumfeld:ebrowse) is still passed as CurrentURIMetaData so the kernel has the renewal URL for its own internal 120-second cycle. Also add proactive BendAVTransportURI every 90 s for actively playing radio rooms so the kernel never reaches its durability limit during normal playback. Co-authored-by: Cursor <cursoragent@cursor.com>
…on (v1.2.31) The 90-second BendAVTransportURI interval in v1.2.30 caused the Raumfeld kernel to reload the live stream on every tick, which the user heard as a brief audio dropout every ~90 seconds. Replace with a single BendAVTransportURI call fired as soon as the real station metadata (raumfeld:ebrowse URL) first arrives from the kernel — typically while the device is still in TRANSITIONING, before any audio has started, so there is zero audible disruption. With the real metadata injected once, the kernel has the ebrowse URL it needs to renew the TuneIn session internally on its own 120-second cycle, matching the behaviour of the native Teufel app. _autoRestartRadio is kept as a last-resort fallback but is suppressed for 10 seconds after the injection to prevent feedback from any brief state transition caused by BendAVTransportURI. Co-authored-by: Cursor <cursoragent@cursor.com>
|
hello, as i have never made a pr - i don't know exactly what i need to do - i changed the file repository.yaml - so i can test all the changes cursor suggested to fix the issues - with my latest commit i/cursor got the following working
so 1.2.31 on raumkernel looks ok so far - i still need some more testing from my wife - that all works from ha the same way as it does from the raumfeld app - at least my automated node-red events do work - as i do have some raumfeld streamers which do need to fire an amp on playback so music can be heard it is the first time i use cursor or myself for any PR - i don't know how you want to proceed - or if you can cherry pick what you think is ok for this/your repo best regards |
…1.2.32) BendAVTransportURI changed AVTransportURI from the station URL to the session-specific <res> URL. Our track-change detector saw this as a URI change, reset _metaInjected, and immediately fired another BendAVTransportURI — creating an infinite reload loop. The same loop prevented station switching: BendAVTransportURI for station A kept overriding any SetAVTransportURI for station B. Fix: remove BendAVTransportURI and all _metaInjected/_metaInjectedAt bookkeeping. The only renewal mechanism is now _autoRestartRadio, which fires once if the kernel drops the stream (~120 s session expiry) and restarts with setAvTransportUri(resUrl, realMeta) so the kernel receives the raumfeld:ebrowse URL and can handle future renewals internally. _resumeAnchorUri is updated before the restart call so the subsequent URI change event is not misidentified as an external track switch. Co-authored-by: Cursor <cursoragent@cursor.com>
…ngle:// When the kernel or native Raumfeld app sets AVTransportURI to a dlna-playsingle:// reference, always call renderer.play() instead of SetAVTransportURI(CDN URL). The kernel manages TuneIn sessions natively and shares one session across all rooms playing the same station — no per-room ebrowse calls, no serial throttle, no 8-110 s drops from stale durability. Refs: three-room drop cascade, stale durability 109 s, Path B CDN-direct corrupting dlna-playsingle:// state. Co-authored-by: Cursor <cursoragent@cursor.com>
When the user selects a station from HA media browser, loadSingle now checks the browse-cache refID for the requested item. If it matches the room's cached station metadata and we have a CDN URL, SetAVTransportURI with the CDN URL and durability=0 is called directly, skipping the slow Tune.ashx session-dispatch endpoint (which can stall for 90+ s when throttled). durability=0 triggers immediate ebrowse (fast, unthrottled) for a fresh CDN session. Response time drops from 90 s to < 2 s. Also: - _parseBrowseXml now stores refID per item for station-match lookups - _zeroDurability() helper: force durability=0 in DIDL metadata - _getItemRefIdFromCache() helper: look up item refID from browse cache Co-authored-by: Cursor <cursoragent@cursor.com>
…~300s drops) Root cause: with 3 rooms each independently calling TuneIn ebrowse every 60s for the same station, the (serial, station) pair hits TuneIn's rate limit after ~5 renewal windows (~300 s). The stream then drops on every room simultaneously. Fix: add _isPermanentCdnUrl() / _stripTuneInMarkers() helpers and apply them in all SetAVTransportURI call sites (Path A, Path B, ECONNRESET retry, loadSingle CDN shortcut). For permanent direct CDN streams (e.g. orf-live.ors-shoutcast.at) the DIDL-Lite TuneIn markers (raumfeld:ebrowse, raumfeld:durability, raumfeld:section, refID) are stripped before passing metadata to the kernel. Without these markers the kernel plays the URL as a plain HTTP stream — no ebrowse calls, no rate-limit exposure, plays indefinitely with any number of rooms. For TuneIn-dispatcher URLs (rndfnk., aggregator=tunein, radiotime.com, etc.) the existing session-management path is unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
…tcut v1.2.100 applied _stripTuneInMarkers only to setAvTransportUri call sites but missed play()'s dlna-playsingle:// guard which fires first and calls bare renderer.play() — causing 3 rooms to each create an independent TuneIn session → 15 ebrowse calls / 5 min → rate-limit → ~280s drops. Fix: inside the dlna-playsingle:// guard, check for a cached permanent CDN URL with matching station refID. If found, call SetAVTransportURI(CDN URL, stripped metadata) so the kernel plays the URL as a plain HTTP stream — no TuneIn calls, no throttle. Falls through to bare play() only when no CDN cache or station mismatch. The kernel retains CurrentTrackURI across sessions even when AVTransportURI is dlna-playsingle://, so _lastSeenCdnUri is always populated at startup. Co-authored-by: Cursor <cursoragent@cursor.com>
Stripping raumfeld:section=RadioTime caused the kernel to treat the stream as a regular media file instead of a live radio broadcast, triggering an internal ~120-150s reconnect timer and dropping the stream at 143s. Fix: keep section=RadioTime so the kernel uses live-radio mode (no Pause, no reconnect timer, plays indefinitely). Block ContentDirectory lookup by changing item id/parentID prefix 0/ → ext/ (non-existent path). Without ebrowse, refID, or a resolvable item id the kernel has no path to call TuneIn — streams the CDN URL directly as live radio forever. Co-authored-by: Cursor <cursoragent@cursor.com>
The CDN server closes TCP connections every ~143 s; the kernel needs raumfeld:ebrowse to renew the session and reconnect. _stripTuneInMarkers was incorrectly removing ebrowse, breaking that renewal path and causing drops at exactly 143 s on every version since v1.2.101. _stripTuneInMarkers now: - KEEPS raumfeld:ebrowse (CDN session renewal — critical) - KEEPS raumfeld:section=RadioTime (live-radio kernel mode) - ZEROS raumfeld:durability → 0 (force immediate ebrowse refresh) - REMOVES <res Tune.ashx?id=…> (session-dispatch, throttled path) - CHANGES id/parentID prefix 0/ → ext/ (blocks ContentDirectory lookup) - STRIPS refID (blocks kernel walk-back to session-dispatch res URL) Co-authored-by: Cursor <cursoragent@cursor.com>
…gle/native-play The root cause of the ~40 s (and shortening) drops was a corruption spiral: - Each play() run read the kernel's already-stripped metadata (no ebrowse, no section=RadioTime, id=ext/) as input for SetAVTransportURI - Sending stripped metadata back to the kernel degraded its state further - The kernel auto-retried the raw CDN connection without TuneIn credentials, each reconnect getting shorter as the CDN grew more hostile Fix: remove all SetAVTransportURI-with-CDN-URL paths from play(). - dlna-playsingle:// state → bare renderer.play() (kernel manages TuneIn) - CDN-URL state → renderer.loadSingle(derivedItemId) to restore the kernel to dlna-playsingle:// with a fresh TuneIn session (ebrowse + section) - ECONNRESET retry now uses loadSingle instead of SetAVTransportURI - loadSingle() CDN shortcut guarded by hasEbrowse: falls through to native loadSingle when cached metadata was corrupted by an older run - _deriveItemIdFromMeta() helper converts ext/X → 0/X for ContentDirectory - room._lastItemId tracked so play() can reload the correct station Co-authored-by: Cursor <cursoragent@cursor.com>
…neIn rate limit Multiple rooms each making independent ebrowse calls every 120 s against the same TuneIn serial caused the API to throttle, dropping streams at ~10 min intervals. Fix: loadSingle() now checks if any other room is already PLAYING the same station (matched via browse-cache refID → TuneIn station ID s-sXXXX). If a match exists, connectRoomToZone() is used to join this room to the existing zone rather than starting a second independent TuneIn session. Mirrors native Raumfeld app behaviour (single zone = single TuneIn session regardless of room count). - room._lastStationId tracked alongside room._lastItemId - Zone join errors fall through to native loadSingle as safe fallback Co-authored-by: Cursor <cursoragent@cursor.com>
…guard Three bugs prevented reliable multi-room zone grouping and clean restarts: 1. Zone-join stationId was derived solely from the browse cache. If a favourite was removed and re-added (new numeric ID), _getItemRefIdFromCache returned null and the zone-join block was silently skipped. Fix: _extractNowPlaying now extracts the TuneIn station ID directly from the live refID attribute in kernel metadata and stores it on room._lastStationId. loadSingle() falls back to scanning other rooms for a matching _lastItemId + _lastStationId pair when the browse cache misses. _lastStationId is reset when the media URI changes. 2. Calling loadSingle() for an item already in dlna-playsingle:// STOPPED state caused the kernel to reply "already active" (HA showed "already active but none was playing"). Fix: loadSingle() detects this case and calls renderer.play() directly. 3. The 60-second dedup guard blocked restarts after a stream drop: if the user tried to reload a dropped station within 60 s, the request was silently ignored. Fix: the guard only fires when TransportState is PLAYING or TRANSITIONING; STOPPED rooms always allow an immediate reload. Co-authored-by: Cursor <cursoragent@cursor.com>
The zone-join logic in v1.2.106 only ran inside loadSingle(). When a room was in STOPPED state with a dlna-playsingle:// URI already loaded, pressing Play in HA used the play() STOPPED→native branch which called renderer.play() directly — skipping the zone-join check. Both rooms then independently created TuneIn sessions for the same station. Fix: the STOPPED→native branch in play() now performs the same zone-join check as loadSingle(): if another room is PLAYING the same station (room._lastStationId match), the room joins the existing zone via zoneManager.connectRoomToZone() rather than restarting independently. Falls back to native Play() on any error. Co-authored-by: Cursor <cursoragent@cursor.com>
When TischlerEi joins Kueche's zone, stop() resolves to the shared zone renderer and calls renderer.stop() which stops the entire zone, silencing Kueche as well. Fix: stop() now checks if the room belongs to a multi-room zone (getRoomCountForZoneUDN > 1). If so it calls dropRoomFromZone() instead of stopping the zone renderer — this ejects just the one room while the others keep playing. Falls back to zone stop on error. Co-authored-by: Cursor <cursoragent@cursor.com>
When the TuneIn CDN session renews (~120 s interval), one physical renderer in a multi-room zone can lose its CDN proxy connection while the zone itself stays alive (the other room continues playing). Three coordinated fixes in RaumkernelHelper.js: 1. Per-room state detection: _extractNowPlaying now parses state.RoomStates (uuid:Kueche=STOPPED,uuid:TischlerEi=PLAYING) to derive the room-specific currState instead of using the zone-level TransportState — so partial drops are detected. 2. Auto-recovery (3 s timer): on partial drop (currState=STOPPED, zone=PLAYING, not user-stopped), drops the room from the zone via dropRoomFromZone() and re-adds it via loadSingle(). The zone-join logic in loadSingle finds the still-playing zone and calls connectRoomToZone() — Kueche rejoins without interrupting TischlerEi. 3. Manual recovery via Play button: play() now checks RoomStates for STOPPED-within-PLAYING and performs the same drop+rejoin instead of a no-op renderer.play(). Co-authored-by: Cursor <cursoragent@cursor.com>
setVolume/setMute now resolve the physical renderer (deviceManager.mediaRenderers.get(room.rendererUdn)) instead of the zone virtual renderer. The zone renderer's SetVolume applies a relative delta to all zone members simultaneously; the physical renderer only adjusts its own hardware amplifier. Partial-drop auto-recovery guards: - prevState === 'PLAYING' check prevents scheduling a rejoin on the initial STOPPED state that occurs when a room first joins a zone (before the kernel promotes it to PLAYING in ~2 s). - _partialDropRejoinPending flag deduplicates the multiple subscription callbacks that fire within ms for the same event, preventing redundant timers. Co-authored-by: Cursor <cursoragent@cursor.com>
Fix: per-room volume was incorrectly showing the zone-master (highest) volume for every member of a zone. _extractNowPlaying now reads the room-specific absolute volume from state.RoomVolumes; negative values (artefact of a prior buggy zone-level delta) are clamped to 0. The device-volume slider in HA now moves independently for each room. Feature: new "Zone Volume" number entity per room. Setting it routes through the virtual zone renderer so all zone members adjust together, matching the native Raumfeld app's group-volume slider. A new setZoneVolume command is added to the addon (RaumkernelHelper.js, index.js) and a set_zone_volume method to api.py. zone_volume is also exposed in the media_player extra_state_attributes. Co-authored-by: Cursor <cursoragent@cursor.com>
The media-player volume slider is now zone-aware: - Grouped: shows/controls the zone-master volume (all members together) - Solo: shows/controls the device volume only Repurpose the number entity as "Device Volume" (unique_id _device_volume) so individual speaker levels within a zone can still be fine-tuned from the device page. The previous "Zone Volume" entity (1.3.0) is replaced. Co-authored-by: Cursor <cursoragent@cursor.com>
For live radio the repeat button (meaningless on a live stream) now toggles the volume slider mode: OFF (default) = this device only ALL = zone master (all grouped members together) HA cycles OFF→ONE→ALL; ONE is silently treated as ALL so the first press immediately activates zone mode (clean two-state toggle). The _zone_volume_mode flag is local and not overwritten by server state updates, so the selection persists while HA is running. Regular music: repeat works as before (OFF/ONE/ALL sent to device). Co-authored-by: Cursor <cursoragent@cursor.com>
1. setZoneVolume: delta via physical renderers with [0,100] clamping — avoids zone renderer delta-based SetVolume which drove volumes negative. 2. setZoneVolumeMode: syncs _zoneVolumeMode to all zone members and broadcasts; nowPlaying.zoneVolumeMode carries the flag to HA so all player cards flip the repeat icon in lock-step. 3. async_set_repeat for live radio now calls set_zone_volume_mode on the server (server round-trip) instead of patching local state — eliminates the race condition that prevented switching back to device mode. Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the repeat-button-as-zone-volume-toggle feature added in 1.3.2/1.3.3. Volume slider now always controls this device only (physical renderer). Repeat and shuffle buttons work normally for all content types. Co-authored-by: Cursor <cursoragent@cursor.com>
SHUFFLE_SET and REPEAT_SET features are now gated on non-live-radio content so the radio card only shows controls meaningful for a live stream. Co-authored-by: Cursor <cursoragent@cursor.com>
…dication Previously _updateZoneMappings stored zone.rooms[].udn values directly in room.zoneMembers. Those UDNs can be virtual-renderer UDNs rather than the roomUdn that the HA entity exposes as room_udn in its state attributes. The group_members property uses that attribute to match against zoneMembers, so a UDN type mismatch caused group_members to always return None and the group indication to never appear in the player view. Fix: resolve each member through _findRoomByAnyUdn and store only the canonical room.roomUdn in zoneMembers. Bumps to v1.3.6. Co-authored-by: Cursor <cursoragent@cursor.com>
- RaumkernelHelper.js: fix ReferenceError in pause() — the renderer variable was never declared, crashing every pause command with "renderer is not defined" (visible in logs after KellerStueberl turn-off this morning) - media_player.py: async_turn_off now calls stop() instead of pause() before entering standby — semantically correct (releases the stream) and avoids triggering the pause bug on devices that are already stopped - Bump version to 1.3.7 in all 4 version files; sync integration bundle and remove stale nested teufel_raumfeld_raumkernel/ duplicate directory - CHANGELOG.md updated Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…isor addon discovery) Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…visor scans recursively for config.yaml and was treating the bundle copy as a second addon (no Dockerfile), causing 'dockerfile is missing' on every update Co-authored-by: Cursor <cursoragent@cursor.com>
…sion.sh - Rename teufel_raumfeld_raumkernel/config.yaml → addon_config_ref.yaml: HA Supervisor recursive config.yaml scan was picking the bundle copy (same slug, no Dockerfile) over the real addon dir after a Supervisor update, causing 'dockerfile is missing' on every addon update attempt - prepare-build.sh: explicitly remove config.yaml from bundle after copy - sync-version.sh: fix sed -i '' (macOS) → sed -i (Linux) Co-authored-by: Cursor <cursoragent@cursor.com>
The Raumfeld kernel sets CurrentTransportActions='Play' after a stream drop but never restarts on its own. Add auto-restart in the addon: after an unintentional PLAYING→STOPPED transition on a live stream, schedule a play() call with an 8 s back-off for short (throttled) sessions and 3 s for longer ones. Guard against user-intentional stops (_userStopped) and partial zone drops (handled by rejoin block). Clear _autoRestartPending on every explicit play() entry point so a manual press cancels any in-flight timer. Co-authored-by: Cursor <cursoragent@cursor.com>
Previous edit accidentally dropped the `try {` opening brace and the
`dropRoomFromZone` call when inserting `_autoRestartPending = false`,
leaving a bare `catch` that caused a SyntaxError on startup.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Hello Holli, Please see my comment here as well: #55 (comment) First of all, impressive to see your hard work on your issues! However I will not be able to review and merge this PR. It contains too many changes instead of a single, focussed change. I understand you used Cursor for creating this changes and don't have a programming background. While AI coding tools can be helpful (and in fact, ha-raumkernel is created by one) the code still needs review and at least high-level checks. Cursor added +14.165 lines of code to this, it will not be possible to review the changes and understand all side effects. If your work helped in your own use case, I suggest to keep your fork of ha-raumkernel and use your own version for your use-case. Best, Uli |
No description provided.