now a working radio controller
This commit is contained in:
@@ -506,6 +506,7 @@ events:
|
||||
|
||||
Volume actions are also available as `denon.volume_up` and
|
||||
`denon.volume_down`. Each call sends five volume steps to the receiver.
|
||||
`denon.volume_mute` explicitly mutes the main output.
|
||||
|
||||
The Denon receiver connection is also configured in `events.yaml`. Power
|
||||
actions use Denon's HTTP control endpoint; set **Settings → Network → Network
|
||||
@@ -534,4 +535,27 @@ After changing `events.yaml`, restart the listener:
|
||||
```bash
|
||||
sudo systemctl restart zigbee-events.service
|
||||
```
|
||||
|
||||
### HEOS favorite stations
|
||||
|
||||
The receiver's HEOS Favorites can be discovered and played through its CLI
|
||||
service on port 1255. The library resolves the player and Favorites source IDs
|
||||
dynamically:
|
||||
|
||||
```bash
|
||||
python3 lib_heos.py list
|
||||
python3 lib_heos.py play 1
|
||||
python3 lib_heos.py play "NPO Radio 2"
|
||||
```
|
||||
|
||||
Favorite numbers are the current 1-based positions shown by `list`. Names are
|
||||
matched case-insensitively. Playing a favorite starts playback on the first
|
||||
HEOS player returned by the receiver.
|
||||
|
||||
`denon.next` checks the AVR power state before changing stations. In standby it
|
||||
starts favorite 1. When powered on, it advances only if the current station is
|
||||
in HEOS Favorites, wrapping the final favorite back to the first. It does
|
||||
nothing when another source or a non-favorite station is playing.
|
||||
`denon.previous` has the same rules in reverse and wraps favorite 1 back to the
|
||||
final favorite.
|
||||
- [RRDtool graph elements](https://oss.oetiker.ch/rrdtool/doc/rrdgraph_graph.en.html)
|
||||
|
||||
+6
-3
@@ -32,9 +32,12 @@ devices:
|
||||
enabled: true
|
||||
events:
|
||||
single_button_1: denon.on
|
||||
single_button_2: denon.off
|
||||
single_button_3: denon.previous
|
||||
single_button_4: denon.next
|
||||
long_button_1: denon.off
|
||||
single_button_2: denon.previous
|
||||
long_button_2: denon.next
|
||||
single_button_3: denon.volume_down
|
||||
long_button_3: denon.volume_mute
|
||||
single_button_4: denon.volume_up
|
||||
|
||||
- name: Device 2
|
||||
topic: zigbee2mqtt/CHANGE_ME_DEVICE_2
|
||||
|
||||
+53
-4
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
@@ -10,6 +11,8 @@ from urllib.error import URLError
|
||||
from urllib.parse import quote
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
import lib_heos
|
||||
|
||||
|
||||
_host = "192.168.178.177"
|
||||
_port = 8080
|
||||
@@ -54,12 +57,44 @@ def off() -> None:
|
||||
_command("PWSTANDBY")
|
||||
|
||||
|
||||
def _power_state() -> str:
|
||||
"""Return ON or STANDBY using Denon's control-protocol status query."""
|
||||
try:
|
||||
with socket.create_connection((_host, 23), timeout=_timeout) as connection:
|
||||
connection.settimeout(_timeout)
|
||||
connection.sendall(b"PW?\r")
|
||||
with connection.makefile("r", encoding="ascii", newline="\r") as stream:
|
||||
while line := stream.readline():
|
||||
response = line.strip()
|
||||
if response.startswith("PW"):
|
||||
state = response[2:]
|
||||
if state in {"ON", "STANDBY"}:
|
||||
return state
|
||||
except OSError as error:
|
||||
raise RuntimeError(f"Cannot query Denon power state: {error}") from error
|
||||
raise RuntimeError("Denon did not return a valid power state")
|
||||
|
||||
|
||||
def next() -> None:
|
||||
"""Select the next preset or source."""
|
||||
"""Play the next HEOS favorite according to the receiver's power state."""
|
||||
lib_heos.configure(_host, timeout=_timeout)
|
||||
if _power_state() == "STANDBY":
|
||||
lib_heos.play_favorite(1)
|
||||
return
|
||||
# Deliberately do nothing when the receiver is on but its current media is
|
||||
# not one of the configured HEOS favorites.
|
||||
lib_heos.next_favorite()
|
||||
|
||||
|
||||
def previous() -> None:
|
||||
"""Select the previous preset or source."""
|
||||
"""Play the previous HEOS favorite according to the receiver's power state."""
|
||||
lib_heos.configure(_host, timeout=_timeout)
|
||||
if _power_state() == "STANDBY":
|
||||
lib_heos.play_favorite(1)
|
||||
return
|
||||
# Deliberately do nothing when the receiver is on but its current media is
|
||||
# not one of the configured HEOS favorites.
|
||||
lib_heos.previous_favorite()
|
||||
|
||||
|
||||
def volume_up() -> None:
|
||||
@@ -74,12 +109,25 @@ def volume_down() -> None:
|
||||
_command("MVDOWN")
|
||||
|
||||
|
||||
def volume_mute() -> None:
|
||||
"""Mute the volume."""
|
||||
_command("MUON")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run a receiver action from the command line."""
|
||||
parser = argparse.ArgumentParser(description="Control the Denon receiver")
|
||||
parser.add_argument(
|
||||
"action",
|
||||
choices=("on", "off", "next", "previous", "volume-up", "volume-down"),
|
||||
choices=(
|
||||
"on",
|
||||
"off",
|
||||
"next",
|
||||
"previous",
|
||||
"volume-up",
|
||||
"volume-down",
|
||||
"volume-mute",
|
||||
),
|
||||
)
|
||||
parser.add_argument("--host", default=_host, help=f"receiver address (default: {_host})")
|
||||
parser.add_argument("--port", type=int, default=_port, help=f"HTTP port (default: {_port})")
|
||||
@@ -116,10 +164,11 @@ def main() -> int:
|
||||
"previous": previous,
|
||||
"volume-up": volume_up,
|
||||
"volume-down": volume_down,
|
||||
"volume-mute": volume_mute,
|
||||
}
|
||||
try:
|
||||
actions[args.action]()
|
||||
except (OSError, URLError) as error:
|
||||
except (OSError, URLError, RuntimeError, lib_heos.HeosError) as error:
|
||||
print(f"Denon {args.action} failed: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
"""Discover and play HEOS favorite stations through the HEOS CLI service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
||||
_host = "192.168.178.177"
|
||||
_port = 1255
|
||||
_timeout = 5.0
|
||||
|
||||
|
||||
class HeosError(RuntimeError):
|
||||
"""Raised when HEOS rejects a command or returns an invalid response."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Favorite:
|
||||
position: int
|
||||
name: str
|
||||
media_id: str
|
||||
|
||||
|
||||
def configure(host: str, port: int = 1255, timeout: float = 5.0) -> None:
|
||||
"""Configure the HEOS device used by subsequent calls."""
|
||||
global _host, _port, _timeout
|
||||
_host = host
|
||||
_port = port
|
||||
_timeout = timeout
|
||||
|
||||
|
||||
def _command(path: str, **parameters: object) -> dict[str, Any]:
|
||||
# HEOS expects the comma in range values literally (e.g. range=0,100).
|
||||
query = urlencode(parameters, safe=",")
|
||||
command = f"heos://{path}" + (f"?{query}" if query else "")
|
||||
try:
|
||||
with socket.create_connection((_host, _port), timeout=_timeout) as connection:
|
||||
connection.settimeout(_timeout)
|
||||
connection.sendall((command + "\r\n").encode("utf-8"))
|
||||
with connection.makefile("r", encoding="utf-8", newline="\n") as stream:
|
||||
while line := stream.readline():
|
||||
try:
|
||||
response = json.loads(line)
|
||||
except json.JSONDecodeError as error:
|
||||
raise HeosError(f"Invalid HEOS response: {line.rstrip()!r}") from error
|
||||
heos = response.get("heos", {})
|
||||
if str(heos.get("command", "")).strip() != path:
|
||||
continue
|
||||
if heos.get("result") != "success":
|
||||
raise HeosError(str(heos.get("message", "HEOS command failed")))
|
||||
message = str(heos.get("message", ""))
|
||||
if "command under process" in message:
|
||||
continue
|
||||
return response
|
||||
except (OSError, TimeoutError) as error:
|
||||
raise HeosError(f"Cannot communicate with HEOS at {_host}:{_port}: {error}") from error
|
||||
raise HeosError(f"HEOS closed the connection without answering {path!r}")
|
||||
|
||||
|
||||
def _player_id() -> str:
|
||||
response = _command("player/get_players")
|
||||
players = response.get("payload", [])
|
||||
if not isinstance(players, list) or not players:
|
||||
raise HeosError("No HEOS players found")
|
||||
return str(players[0]["pid"])
|
||||
|
||||
|
||||
def _favorites_source_id() -> str:
|
||||
response = _command("browse/get_music_sources")
|
||||
sources = response.get("payload", [])
|
||||
if not isinstance(sources, list):
|
||||
raise HeosError("HEOS returned an invalid music-source list")
|
||||
for source in sources:
|
||||
if str(source.get("name", "")).casefold() == "favorites":
|
||||
if source.get("available") == "false":
|
||||
raise HeosError("The HEOS Favorites source is unavailable")
|
||||
return str(source["sid"])
|
||||
raise HeosError("HEOS Favorites source not found")
|
||||
|
||||
|
||||
def list_favorites() -> list[Favorite]:
|
||||
"""Return the currently configured HEOS favorite stations."""
|
||||
response = _command("browse/browse", sid=_favorites_source_id(), range="0,100")
|
||||
payload = response.get("payload", [])
|
||||
if not isinstance(payload, list):
|
||||
raise HeosError("HEOS returned an invalid Favorites list")
|
||||
return [
|
||||
Favorite(position=index, name=str(item["name"]), media_id=str(item.get("mid", "")))
|
||||
for index, item in enumerate(payload, start=1)
|
||||
if item.get("playable") == "yes"
|
||||
]
|
||||
|
||||
|
||||
def _find_current_favorite(favorites: list[Favorite], payload: object) -> Favorite | None:
|
||||
if not isinstance(payload, dict) or payload.get("type") != "station":
|
||||
return None
|
||||
current_ids = {
|
||||
str(payload.get(key, "")).strip()
|
||||
for key in ("album_id", "mid")
|
||||
if payload.get(key)
|
||||
}
|
||||
for favorite in favorites:
|
||||
if favorite.media_id and favorite.media_id in current_ids:
|
||||
return favorite
|
||||
station = str(payload.get("station", "")).strip().casefold()
|
||||
if station:
|
||||
return next((item for item in favorites if item.name.casefold() == station), None)
|
||||
return None
|
||||
|
||||
|
||||
def current_favorite() -> Favorite | None:
|
||||
"""Return the playing HEOS favorite, or None for other/no playback."""
|
||||
favorites = list_favorites()
|
||||
player_id = _player_id()
|
||||
response = _command("player/get_now_playing_media", pid=player_id)
|
||||
return _find_current_favorite(favorites, response.get("payload"))
|
||||
|
||||
|
||||
def next_favorite() -> Favorite | None:
|
||||
"""Play the favorite after the current one, wrapping at the end."""
|
||||
favorites = list_favorites()
|
||||
if not favorites:
|
||||
raise HeosError("No HEOS favorites configured")
|
||||
player_id = _player_id()
|
||||
response = _command("player/get_now_playing_media", pid=player_id)
|
||||
current = _find_current_favorite(favorites, response.get("payload"))
|
||||
if current is None:
|
||||
return None
|
||||
current_index = favorites.index(current)
|
||||
selected = favorites[(current_index + 1) % len(favorites)]
|
||||
_command("browse/play_preset", pid=player_id, preset=selected.position)
|
||||
return selected
|
||||
|
||||
|
||||
def previous_favorite() -> Favorite | None:
|
||||
"""Play the favorite before the current one, wrapping at the start."""
|
||||
favorites = list_favorites()
|
||||
if not favorites:
|
||||
raise HeosError("No HEOS favorites configured")
|
||||
player_id = _player_id()
|
||||
response = _command("player/get_now_playing_media", pid=player_id)
|
||||
current = _find_current_favorite(favorites, response.get("payload"))
|
||||
if current is None:
|
||||
return None
|
||||
current_index = favorites.index(current)
|
||||
selected = favorites[(current_index - 1) % len(favorites)]
|
||||
_command("browse/play_preset", pid=player_id, preset=selected.position)
|
||||
return selected
|
||||
|
||||
|
||||
def play_favorite(favorite: str | int) -> Favorite:
|
||||
"""Play a favorite selected by 1-based position or case-insensitive name."""
|
||||
favorites = list_favorites()
|
||||
selected: Favorite | None = None
|
||||
if isinstance(favorite, int) or str(favorite).strip().isdigit():
|
||||
position = int(favorite)
|
||||
selected = next((item for item in favorites if item.position == position), None)
|
||||
else:
|
||||
requested_name = str(favorite).strip().casefold()
|
||||
selected = next((item for item in favorites if item.name.casefold() == requested_name), None)
|
||||
if selected is None:
|
||||
raise HeosError(f"HEOS favorite not found: {favorite!r}")
|
||||
_command("browse/play_preset", pid=_player_id(), preset=selected.position)
|
||||
return selected
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--host", default=_host, help=f"HEOS address (default: {_host})")
|
||||
parser.add_argument("--port", type=int, default=_port, help=f"HEOS CLI port (default: {_port})")
|
||||
parser.add_argument("--timeout", type=float, default=_timeout)
|
||||
subparsers = parser.add_subparsers(dest="action", required=True)
|
||||
subparsers.add_parser("list", help="list available HEOS favorites")
|
||||
play_parser = subparsers.add_parser("play", help="play a favorite by number or name")
|
||||
play_parser.add_argument("favorite")
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.port <= 65535:
|
||||
parser.error("--port must be between 1 and 65535")
|
||||
if args.timeout <= 0:
|
||||
parser.error("--timeout must be positive")
|
||||
configure(args.host, args.port, args.timeout)
|
||||
try:
|
||||
if args.action == "list":
|
||||
for favorite in list_favorites():
|
||||
print(f"{favorite.position:2}. {favorite.name}")
|
||||
else:
|
||||
selected = play_favorite(args.favorite)
|
||||
print(f"Playing HEOS favorite {selected.position}: {selected.name}")
|
||||
except HeosError as error:
|
||||
print(f"HEOS {args.action} failed: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -11,3 +11,51 @@
|
||||
2026-08-22 14:03:19,925 INFO called device="Radio Orb" event="single_button_1" function=denon.on
|
||||
2026-08-22 14:10:26,225 INFO Stopping on signal 2
|
||||
2026-08-22 14:10:26,226 INFO Zigbee event listener stopped
|
||||
2026-08-22 14:24:28,064 INFO Starting Zigbee event listener
|
||||
2026-08-22 14:24:28,071 INFO Connected to MQTT broker 192.168.178.247:1883; listening to 1 device(s)
|
||||
2026-08-22 14:24:28,116 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_1" payload={"action":"single_button_1","battery":100,"linkquality":33,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:24:28,140 INFO called device="Radio Orb" event="single_button_1" function=denon.on
|
||||
2026-08-22 14:24:50,076 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_2" payload={"action":"single_button_2","battery":100,"linkquality":3,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:24:50,083 INFO called device="Radio Orb" event="single_button_2" function=denon.off
|
||||
2026-08-22 14:40:02,927 INFO Stopping on signal 2
|
||||
2026-08-22 14:40:02,929 INFO Zigbee event listener stopped
|
||||
2026-08-22 14:41:32,416 INFO Starting Zigbee event listener
|
||||
2026-08-22 14:41:32,424 INFO Connected to MQTT broker 192.168.178.247:1883; listening to 1 device(s)
|
||||
2026-08-22 14:41:32,467 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_2" payload={"action":"single_button_2","battery":100,"linkquality":3,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:41:32,506 INFO called device="Radio Orb" event="single_button_2" function=denon.volume_down
|
||||
2026-08-22 14:41:39,239 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="long_button_2" payload={"action":"long_button_2","battery":100,"linkquality":24,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:41:39,301 INFO called device="Radio Orb" event="long_button_2" function=denon.volume_up
|
||||
2026-08-22 14:41:41,903 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="long_button_2" payload={"action":"long_button_2","battery":100,"linkquality":33,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:41:41,948 INFO called device="Radio Orb" event="long_button_2" function=denon.volume_up
|
||||
2026-08-22 14:41:44,462 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="long_button_2" payload={"action":"long_button_2","battery":100,"linkquality":33,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:41:44,542 INFO called device="Radio Orb" event="long_button_2" function=denon.volume_up
|
||||
2026-08-22 14:49:15,684 INFO Stopping on signal 2
|
||||
2026-08-22 14:49:15,685 INFO Zigbee event listener stopped
|
||||
2026-08-22 14:49:17,774 INFO Starting Zigbee event listener
|
||||
2026-08-22 14:49:17,779 INFO Connected to MQTT broker 192.168.178.247:1883; listening to 1 device(s)
|
||||
2026-08-22 14:49:17,824 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="long_button_2" payload={"action":"long_button_2","battery":100,"linkquality":33,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:49:17,876 INFO called device="Radio Orb" event="long_button_2" function=denon.volume_up
|
||||
2026-08-22 14:49:25,063 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_4" payload={"action":"single_button_4","battery":100,"linkquality":21,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:49:26,189 INFO called device="Radio Orb" event="single_button_4" function=denon.next
|
||||
2026-08-22 14:55:07,851 INFO Stopping on signal 2
|
||||
2026-08-22 14:55:07,852 INFO Zigbee event listener stopped
|
||||
2026-08-22 14:55:09,373 INFO Starting Zigbee event listener
|
||||
2026-08-22 14:55:09,379 INFO Connected to MQTT broker 192.168.178.247:1883; listening to 1 device(s)
|
||||
2026-08-22 14:55:09,423 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_4" payload={"action":"single_button_4","battery":100,"linkquality":21,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:55:09,528 INFO called device="Radio Orb" event="single_button_4" function=denon.volume_up
|
||||
2026-08-22 14:55:19,370 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_4" payload={"action":"single_button_4","battery":100,"linkquality":15,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:55:19,411 INFO called device="Radio Orb" event="single_button_4" function=denon.volume_up
|
||||
2026-08-22 14:55:20,701 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_4" payload={"action":"single_button_4","battery":100,"linkquality":3,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:55:20,803 INFO called device="Radio Orb" event="single_button_4" function=denon.volume_up
|
||||
2026-08-22 14:55:23,159 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_4" payload={"action":"single_button_4","battery":100,"linkquality":0,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:55:23,198 INFO called device="Radio Orb" event="single_button_4" function=denon.volume_up
|
||||
2026-08-22 14:55:25,617 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="long_button_3" payload={"action":"long_button_3","battery":100,"linkquality":12,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:55:25,625 INFO called device="Radio Orb" event="long_button_3" function=denon.volume_mute
|
||||
2026-08-22 14:55:29,407 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_4" payload={"action":"single_button_4","battery":100,"linkquality":21,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:55:29,455 INFO called device="Radio Orb" event="single_button_4" function=denon.volume_up
|
||||
2026-08-22 14:55:33,707 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_3" payload={"action":"single_button_3","battery":100,"linkquality":15,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:55:33,887 INFO called device="Radio Orb" event="single_button_3" function=denon.volume_down
|
||||
2026-08-22 14:55:35,857 INFO event device="Radio Orb" topic=zigbee2mqtt/button1_radio action="single_button_2" payload={"action":"single_button_2","battery":100,"linkquality":27,"update":{"installed_version":4101,"latest_release_notes":null,"latest_source":"https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-01m_v1.1.0.ota","latest_version":4352,"state":"available"}}
|
||||
2026-08-22 14:55:37,085 INFO called device="Radio Orb" event="single_button_2" function=denon.previous
|
||||
2026-08-22 14:58:41,282 INFO Stopping on signal 2
|
||||
2026-08-22 14:58:41,283 INFO Zigbee event listener stopped
|
||||
|
||||
@@ -29,6 +29,7 @@ FUNCTIONS = {
|
||||
"denon.previous": lib_denon.previous,
|
||||
"denon.volume_up": lib_denon.volume_up,
|
||||
"denon.volume_down": lib_denon.volume_down,
|
||||
"denon.volume_mute": lib_denon.volume_mute,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user