jmri_server.services.audio

AudioService — JSON topic audio.

Wire shape per jmri/server/json/audio/audio-server.json and the JMRI Java handler JsonAudioHttpService (case 16 STOPPED, case 17 PLAYING, command = Play / Stop on post).

 1"""``AudioService`` — JSON topic ``audio``.
 2
 3Wire shape per ``jmri/server/json/audio/audio-server.json`` and the
 4JMRI Java handler ``JsonAudioHttpService`` (case 16 STOPPED, case 17
 5PLAYING, ``command`` = Play / Stop on post).
 6"""
 7from __future__ import annotations
 8
 9from typing import Any
10
11from jmri_core.beans.audio import (
12    AUDIO_STATE_PLAYING,
13    AUDIO_STATE_STOPPED,
14    Audio,
15    AudioManager,
16)
17
18from jmri_server.service import JsonServiceError
19from jmri_server.services.beans import _BeanService, _common_named_bean
20
21
22class AudioService(_BeanService):
23    topic = "audio"
24
25    def manager(self) -> AudioManager:
26        return self.ctx.connection.audios
27
28    def project(self, bean: Audio) -> dict[str, Any]:
29        return _common_named_bean(bean) | {
30            "state": bean.state,
31            "kind": bean.kind,
32            "url": bean.url,
33            "gain": bean.gain,
34        }
35
36    async def apply(self, bean: Audio, data: dict[str, Any]) -> None:
37        if "kind" in data:
38            kind = data["kind"]
39            if kind not in ("source", "buffer", "listener"):
40                raise JsonServiceError(400, f"invalid audio kind: {kind!r}")
41            bean.kind = kind
42        if "url" in data:
43            bean.url = str(data["url"]) if data["url"] is not None else None
44        if "gain" in data:
45            try:
46                gain = float(data["gain"])
47            except (TypeError, ValueError):
48                raise JsonServiceError(400, "audio.gain must be numeric") from None
49            if not 0.0 <= gain <= 1.0:
50                raise JsonServiceError(400, "audio.gain must be in [0.0, 1.0]")
51            bean.gain = gain
52        # Explicit state override (rare — clients normally send
53        # ``command`` instead) but scripts sometimes set state directly.
54        if "state" in data:
55            state = int(data["state"])
56            if state not in (AUDIO_STATE_STOPPED, AUDIO_STATE_PLAYING):
57                raise JsonServiceError(
58                    400,
59                    f"audio.state must be {AUDIO_STATE_STOPPED} (stopped) or "
60                    f"{AUDIO_STATE_PLAYING} (playing)",
61                )
62            if state == AUDIO_STATE_PLAYING:
63                await bean.play()
64            else:
65                await bean.stop()
66        # JMRI Java's Play / Stop verbs. Only meaningful on sources; a
67        # buffer or listener silently no-ops the command rather than
68        # 400-ing to keep the API forgiving for polymorphic clients.
69        command = data.get("command")
70        if command == "Play":
71            if bean.kind == "source":
72                await bean.play()
73        elif command == "Stop":
74            if bean.kind == "source":
75                await bean.stop()
76        elif command is not None:
77            raise JsonServiceError(400, f"unknown audio command: {command!r}")
class AudioService(jmri_server.services.beans._BeanService):
23class AudioService(_BeanService):
24    topic = "audio"
25
26    def manager(self) -> AudioManager:
27        return self.ctx.connection.audios
28
29    def project(self, bean: Audio) -> dict[str, Any]:
30        return _common_named_bean(bean) | {
31            "state": bean.state,
32            "kind": bean.kind,
33            "url": bean.url,
34            "gain": bean.gain,
35        }
36
37    async def apply(self, bean: Audio, data: dict[str, Any]) -> None:
38        if "kind" in data:
39            kind = data["kind"]
40            if kind not in ("source", "buffer", "listener"):
41                raise JsonServiceError(400, f"invalid audio kind: {kind!r}")
42            bean.kind = kind
43        if "url" in data:
44            bean.url = str(data["url"]) if data["url"] is not None else None
45        if "gain" in data:
46            try:
47                gain = float(data["gain"])
48            except (TypeError, ValueError):
49                raise JsonServiceError(400, "audio.gain must be numeric") from None
50            if not 0.0 <= gain <= 1.0:
51                raise JsonServiceError(400, "audio.gain must be in [0.0, 1.0]")
52            bean.gain = gain
53        # Explicit state override (rare — clients normally send
54        # ``command`` instead) but scripts sometimes set state directly.
55        if "state" in data:
56            state = int(data["state"])
57            if state not in (AUDIO_STATE_STOPPED, AUDIO_STATE_PLAYING):
58                raise JsonServiceError(
59                    400,
60                    f"audio.state must be {AUDIO_STATE_STOPPED} (stopped) or "
61                    f"{AUDIO_STATE_PLAYING} (playing)",
62                )
63            if state == AUDIO_STATE_PLAYING:
64                await bean.play()
65            else:
66                await bean.stop()
67        # JMRI Java's Play / Stop verbs. Only meaningful on sources; a
68        # buffer or listener silently no-ops the command rather than
69        # 400-ing to keep the API forgiving for polymorphic clients.
70        command = data.get("command")
71        if command == "Play":
72            if bean.kind == "source":
73                await bean.play()
74        elif command == "Stop":
75            if bean.kind == "source":
76                await bean.stop()
77        elif command is not None:
78            raise JsonServiceError(400, f"unknown audio command: {command!r}")

Base class encapsulating get/list/put/post/delete plumbing.

Concrete bean services set topic and override manager(), project(), and (when mutations are supported) apply().

topic = 'audio'
def manager(self) -> jmri_core.beans.audio.AudioManager:
26    def manager(self) -> AudioManager:
27        return self.ctx.connection.audios
def project(self, bean: jmri_core.beans.audio.Audio) -> dict[str, typing.Any]:
29    def project(self, bean: Audio) -> dict[str, Any]:
30        return _common_named_bean(bean) | {
31            "state": bean.state,
32            "kind": bean.kind,
33            "url": bean.url,
34            "gain": bean.gain,
35        }
async def apply( self, bean: jmri_core.beans.audio.Audio, data: dict[str, typing.Any]) -> None:
37    async def apply(self, bean: Audio, data: dict[str, Any]) -> None:
38        if "kind" in data:
39            kind = data["kind"]
40            if kind not in ("source", "buffer", "listener"):
41                raise JsonServiceError(400, f"invalid audio kind: {kind!r}")
42            bean.kind = kind
43        if "url" in data:
44            bean.url = str(data["url"]) if data["url"] is not None else None
45        if "gain" in data:
46            try:
47                gain = float(data["gain"])
48            except (TypeError, ValueError):
49                raise JsonServiceError(400, "audio.gain must be numeric") from None
50            if not 0.0 <= gain <= 1.0:
51                raise JsonServiceError(400, "audio.gain must be in [0.0, 1.0]")
52            bean.gain = gain
53        # Explicit state override (rare — clients normally send
54        # ``command`` instead) but scripts sometimes set state directly.
55        if "state" in data:
56            state = int(data["state"])
57            if state not in (AUDIO_STATE_STOPPED, AUDIO_STATE_PLAYING):
58                raise JsonServiceError(
59                    400,
60                    f"audio.state must be {AUDIO_STATE_STOPPED} (stopped) or "
61                    f"{AUDIO_STATE_PLAYING} (playing)",
62                )
63            if state == AUDIO_STATE_PLAYING:
64                await bean.play()
65            else:
66                await bean.stop()
67        # JMRI Java's Play / Stop verbs. Only meaningful on sources; a
68        # buffer or listener silently no-ops the command rather than
69        # 400-ing to keep the API forgiving for polymorphic clients.
70        command = data.get("command")
71        if command == "Play":
72            if bean.kind == "source":
73                await bean.play()
74        elif command == "Stop":
75            if bean.kind == "source":
76                await bean.stop()
77        elif command is not None:
78            raise JsonServiceError(400, f"unknown audio command: {command!r}")

Apply mutations from a client request. Default: no-op.