From 2b827730709ac7c681ff9ce3a1b185d933097892 Mon Sep 17 00:00:00 2001
From: loosrob <79396812+loosrob@users.noreply.github.com>
Date: Fri, 30 Jul 2021 14:47:56 +0200
Subject: [PATCH 1/8] Nymea plugins: add Yamaha AVR plugin
add control for (non-Musiccast) Yamaha network receivers
---
yamahaavr/README.md | 28 +
yamahaavr/integrationpluginyamaha.json | 754 +++++++++++++
yamahaavr/integrationpluginyamaha.py | 1382 ++++++++++++++++++++++++
yamahaavr/meta.json | 13 +
yamahaavr/requirements.txt | 64 ++
yamahaavr/yamaha.png | Bin 0 -> 12812 bytes
6 files changed, 2241 insertions(+)
create mode 100644 yamahaavr/README.md
create mode 100644 yamahaavr/integrationpluginyamaha.json
create mode 100644 yamahaavr/integrationpluginyamaha.py
create mode 100644 yamahaavr/meta.json
create mode 100644 yamahaavr/requirements.txt
create mode 100644 yamahaavr/yamaha.png
diff --git a/yamahaavr/README.md b/yamahaavr/README.md
new file mode 100644
index 00000000..24174267
--- /dev/null
+++ b/yamahaavr/README.md
@@ -0,0 +1,28 @@
+# Yamaha AV Receiver
+
+This plugin allows to control your Yamaha (non-MusicCast) AV receivers.
+
+Each supported receiver on your local area network should appear automatically in the system.
+
+Browsing is supported, but can be slow due to the nature of the Yamaha API.
+As a nice extra, a random album on a random server can be started with a simple action.
+
+## Supported Things
+
+* Yamaha RX-V675 (tested)
+* Other non-MusicCast Yamaha RX-V devices should also work, but haven't been tested
+* Newer Yamaha MusicCast devices aren't supported, as they use a different API
+
+## Manual
+
+* Volumes (and some other variables) are represented by integer in the Yamaha API, but shown as double = int/10 in Yamaha UI, so e.g. API will show -455, but receiver will show -45.5 dB. As the Nymea media interface currently needs volume to be an integer, the plugin will show volume as integer, so e.g. -455 instead of -45.5 dB
+* Browsing shortcuts (for browsing a SERVER source) can be added via the Thing settings, to avoid having to go through e.g. ServerName/Music/By Folder/FolderName each time. By adding a "shortcut tree" such as ServerName/Music/By Folder/FolderName to the settings, browsing will start in folder FolderName instead of first showing a list of servers including ServerName; another possibility is e.g. ServerName2/Music/By Album
+
+## Requirements
+
+* nymea and the Yamaha device must be in the same local area network.
+* The package "nymea-plugin-yamaha" must be installed.
+
+## More
+
+ [Yamaha Electronics](https://www.yamaha.com/en/)
diff --git a/yamahaavr/integrationpluginyamaha.json b/yamahaavr/integrationpluginyamaha.json
new file mode 100644
index 00000000..cc86df4e
--- /dev/null
+++ b/yamahaavr/integrationpluginyamaha.json
@@ -0,0 +1,754 @@
+{
+ "displayName": "Yamaha AV receiver",
+ "name": "Yamaha",
+ "id": "aa07a5cc-ca65-4043-9e61-e7040a6a60ff",
+ "vendors": [
+ {
+ "id": "07460546-0b4a-4c3c-9143-d5de6b09de71",
+ "displayName": "Yamaha Corporation",
+ "name": "yamaha",
+ "thingClasses": [
+ {
+ "id": "f799a98a-8521-451e-a206-4b60e2dd0985",
+ "name": "receiver",
+ "displayName": "Yamaha AV Receiver",
+ "createMethods": ["discovery"],
+ "interfaces": ["mediaplayer", "mediacontroller", "volumecontroller", "mediametadataprovider", "connectable", "power"],
+ "browsable": true,
+ "paramTypes": [
+ {
+ "id": "0129f3eb-3cc6-47fa-9366-74194ec655f9",
+ "name": "serial",
+ "displayName": "Serial Number",
+ "type": "QString",
+ "readOnly": true
+ }
+ ],
+ "settingsTypes": [
+ {
+ "id": "e7a1e300-f3e9-4b56-a320-ed347409da9c",
+ "name": "browsingShortcut1",
+ "displayName": "Browsing shortcut 1 (see readme)",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "98fea0de-82a5-40af-bd98-35099555c639",
+ "name": "shortcutLabel1",
+ "displayName": "Shortcut 1 label",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "ca923c89-fdf9-4d3d-b1e7-947530450129",
+ "name": "browsingShortcut2",
+ "displayName": "Browsing shortcut 2",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "2aa92119-8a84-4ad0-8115-3e040ce1b54a",
+ "name": "shortcutLabel2",
+ "displayName": "Shortcut 2 label",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "62747f24-f706-4f40-9a3a-28a444125e59",
+ "name": "browsingShortcut3",
+ "displayName": "Browsing shortcut 3",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "bdc440e9-8340-406f-9c86-32fd3795031a",
+ "name": "shortcutLabel3",
+ "displayName": "Shortcut 3 label",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "28549b0d-c006-47ca-b215-3a86b1ed5440",
+ "name": "browsingShortcut4",
+ "displayName": "Browsing shortcut 4",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "e35d5f98-3ea6-488c-af41-a1b7635f1cb3",
+ "name": "shortcutLabel4",
+ "displayName": "Shortcut 4 label",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "ab9e7218-5673-43e1-80a8-d35125dfa5a7",
+ "name": "browsingShortcut5",
+ "displayName": "Browsing shortcut 5",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "15793b6f-e2f6-47c4-96f7-494eac77a61e",
+ "name": "shortcutLabel5",
+ "displayName": "Shortcut 5 label",
+ "type": "QString",
+ "defaultValue": ""
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "ea3ccab0-549a-4174-a6e1-493981cf5fa8",
+ "name": "connected",
+ "displayName": "Connected",
+ "displayNameEvent": "Connected changed",
+ "defaultValue": false,
+ "type": "bool",
+ "cached": false
+ },
+ {
+ "displayName": "Power",
+ "id": "5b1232a6-4eb5-4e7a-825b-4906de623ea0",
+ "name": "power",
+ "displayNameEvent": "Power changed",
+ "displayNameAction": "Set power",
+ "type": "bool",
+ "defaultValue": false,
+ "writable": true
+ },
+ {
+ "displayName": "Sleep",
+ "id": "8090c8ea-dea5-45d0-9592-092c50ee4235",
+ "name": "sleep",
+ "displayNameEvent": "Sleep timer changed",
+ "displayNameAction": "Set sleep timer",
+ "type": "QString",
+ "writable": true,
+ "possibleValues": [
+ "Off",
+ "30 min",
+ "60 min",
+ "90 min",
+ "120 min"
+ ],
+ "defaultValue": "Off"
+ },
+ {
+ "displayName": "Mute",
+ "id": "f36a7bac-313e-4ab2-8816-4d9ce3684414",
+ "name": "mute",
+ "displayNameEvent": "Mute changed",
+ "displayNameAction": "Set mute",
+ "type": "bool",
+ "defaultValue": false,
+ "writable": true
+ },
+ {
+ "displayName": "Volume",
+ "id": "9734a504-7447-4c82-ba90-d47fbe50a696",
+ "name": "volume",
+ "displayNameEvent": "Volume changed",
+ "displayNameAction": "Set volume",
+ "type": "int",
+ "defaultValue": -500,
+ "minValue": -800,
+ "maxValue": -100,
+ "writable": true
+ },
+ {
+ "displayName": "Input Source",
+ "id": "710949d8-ae7c-4a60-9cfa-a9f0eb2ffbe1",
+ "name": "inputSource",
+ "displayNameEvent": "Input source changed",
+ "displayNameAction": "Set input source",
+ "type": "QString",
+ "writable": true,
+ "possibleValues": [
+ "TUNER",
+ "NAPSTER",
+ "SPOTIFY",
+ "SERVER",
+ "AirPlay",
+ "NET RADIO",
+ "USB",
+ "HDMI1",
+ "HDMI2",
+ "HDMI3",
+ "HDMI4",
+ "HDMI5",
+ "AV1",
+ "AV2",
+ "AV3",
+ "AV4",
+ "AV5",
+ "AV6",
+ "V-AUX",
+ "AUDIO1",
+ "AUDIO2"
+ ],
+ "defaultValue": "HDMI1"
+ },
+ {
+ "displayName": "Surround Program",
+ "id": "1a9b6260-e71b-4750-bf03-34bca0b34222",
+ "name": "surroundMode",
+ "displayNameEvent": "Surround program changed",
+ "displayNameAction": "Set surround program",
+ "type": "QString",
+ "writable": true,
+ "possibleValues": [
+ "Standard",
+ "2ch Stereo",
+ "7ch Stereo",
+ "Straight",
+ "Surround Decoder",
+ "Sci-Fi",
+ "Adventure",
+ "Drama",
+ "Spectacle",
+ "Mono Movie",
+ "Music Video",
+ "Roleplaying Game",
+ "Action Game",
+ "Sports",
+ "Hall in Munich",
+ "Hall in Vienna",
+ "Chamber",
+ "Cellar Club",
+ "The Roxy Theatre",
+ "The Bottom Line"
+ ],
+ "defaultValue": "Standard"
+ },
+ {
+ "displayName": "Bass",
+ "id": "07b2c6ae-74c6-4cb2-b434-a25ad90fb7f9",
+ "name": "bass",
+ "displayNameEvent": "Bass changed",
+ "displayNameAction": "Set bass",
+ "type": "int",
+ "defaultValue": 0,
+ "minValue": -60,
+ "maxValue": 60,
+ "writable": true
+ },
+ {
+ "displayName": "Treble",
+ "id": "c06bce86-c1cb-42f0-9891-161a7f0849f5",
+ "name": "treble",
+ "displayNameEvent": "Treble changed",
+ "displayNameAction": "Set treble",
+ "type": "int",
+ "defaultValue": 0,
+ "minValue": -60,
+ "maxValue": 60,
+ "writable": true
+ },
+ {
+ "displayName": "Subwoofer Trim",
+ "id": "8b77db08-9a94-466c-8a57-b7cd8e638a80",
+ "name": "subwooferTrim",
+ "displayNameEvent": "Subwoofer trim changed",
+ "displayNameAction": "Set subwoofer Trim",
+ "type": "int",
+ "defaultValue": 0,
+ "minValue": -60,
+ "maxValue": 60,
+ "writable": true
+ },
+ {
+ "displayName": "Pure direct",
+ "id": "d19ddece-6bf5-4c2d-9bbc-018b02d564df",
+ "name": "pureDirect",
+ "displayNameEvent": "Pure direct changed",
+ "displayNameAction": "Set pure direct",
+ "type": "bool",
+ "defaultValue": false,
+ "writable": true
+ },
+ {
+ "displayName": "Enhancer",
+ "id": "ca7a4564-9f6d-4f07-8f63-445e44a0eb93",
+ "name": "enhancer",
+ "displayNameEvent": "Enhancer changed",
+ "displayNameAction": "Set enhancer",
+ "type": "bool",
+ "defaultValue": false,
+ "writable": true
+ },
+ {
+ "displayName": "Cinema DSP 3D",
+ "id": "8504e78f-924b-492f-8cc7-69290fb29c95",
+ "name": "cinemaDSP3D",
+ "displayNameEvent": "Cinema DSP 3D changed",
+ "displayNameAction": "Set Cinema DSP 3D",
+ "type": "QString",
+ "writable": true,
+ "possibleValues": [
+ "Off",
+ "Auto"
+ ],
+ "defaultValue": "Auto"
+ },
+ {
+ "displayName": "Adaptive DRC",
+ "id": "99057bc6-4d2a-4670-a1c7-02e9e869b86b",
+ "name": "adaptiveDRC",
+ "displayNameEvent": "Adaptive DRC changed",
+ "displayNameAction": "Set Adaptive DRC",
+ "type": "QString",
+ "writable": true,
+ "possibleValues": [
+ "Off",
+ "Auto"
+ ],
+ "defaultValue": "Auto"
+ },
+ {
+ "displayName": "Dialogue level",
+ "id": "042ed95f-d0f7-4eda-b469-a7863a639e84",
+ "name": "dialogueLevel",
+ "displayNameEvent": "Dialogue level changed",
+ "displayNameAction": "Set dialogue level",
+ "type": "int",
+ "defaultValue": 0,
+ "minValue": 0,
+ "maxValue": 3,
+ "writable": true
+ },
+ {
+ "displayName": "Dialogue lift",
+ "id": "3eff11b2-f6b0-4180-aadc-ef77fca633f4",
+ "name": "dialogueLift",
+ "displayNameEvent": "Dialogue lift changed",
+ "displayNameAction": "Set dialogue lift",
+ "type": "int",
+ "defaultValue": 0,
+ "minValue": 0,
+ "maxValue": 5,
+ "writable": true
+ },
+ {
+ "id": "053c01cd-4696-490b-91ad-480cd90171e0",
+ "name": "artist",
+ "displayName": "Artist",
+ "displayNameEvent": "Artist changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "3db27780-b0a0-409e-9986-2c751f0bb9d8",
+ "name": "collection",
+ "displayName": "Album",
+ "displayNameEvent": "Album changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "1686f85c-ce71-474b-8ece-e8da97fb809b",
+ "name": "title",
+ "displayName": "Title",
+ "displayNameEvent": "Title changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "e321c171-b0c1-486f-a245-b4fd8ddf90e3",
+ "name": "artwork",
+ "displayName": "Artwork",
+ "displayNameEvent": "Artwork changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "f936b76f-657d-467a-90c0-cbdf08de2201",
+ "name": "playerType",
+ "displayName": "Player type",
+ "displayNameEvent": "Player type changed",
+ "possibleValues": [
+ "audio",
+ "video"
+ ],
+ "type": "QString",
+ "defaultValue": "audio"
+ },
+ {
+ "id": "f5a12603-5df9-4eae-9096-17775272d22f",
+ "name": "playbackStatus",
+ "displayName": "Playback status",
+ "displayNameEvent": "Playback status changed",
+ "displayNameAction": "Set playback status",
+ "type": "QString",
+ "defaultValue": "Stopped",
+ "possibleValues": ["Playing", "Paused", "Stopped"],
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "189600dd-6cdc-4453-a816-f897b8ef06dd",
+ "name": "shuffle",
+ "displayName": "Shuffle",
+ "displayNameEvent": "Shuffle changed",
+ "displayNameAction": "Set shuffle",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "fa5e8258-ce4c-4afa-b8bc-9c3c2be7653b",
+ "name": "repeat",
+ "displayName": "Repeat mode",
+ "displayNameEvent": "Repeat mode changed",
+ "displayNameAction": "Set repeat mode",
+ "type": "QString",
+ "defaultValue": "None",
+ "possibleValues": ["None", "One", "All"],
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "b269c1f4-1881-461d-810b-b594de499490",
+ "name": "url",
+ "displayName": "Device Address",
+ "displayNameEvent": "Device IP changed",
+ "defaultValue": "0.0.0.0",
+ "type" : "QString"
+ }
+ ],
+ "actionTypes": [
+ {
+ "id": "671abe60-b8a5-48cd-a138-9f4283a823d2",
+ "name": "randomAlbum",
+ "displayName": "Play Random Album"
+ },
+ {
+ "id": "dd1a44d1-ecb2-4752-a8d4-f3960dec6808",
+ "displayName": "Increase volume",
+ "name": "increaseVolume",
+ "paramTypes": [
+ {
+ "id": "3bb5ff93-a787-4a30-957e-b96f27da2199",
+ "name": "step",
+ "displayName": "Step size",
+ "type": "uint",
+ "minValue": 1,
+ "maxValue": 5,
+ "defaultValue": 5
+ }
+ ]
+ },
+ {
+ "id": "d3752c32-92e3-4396-8e2f-ab5e57c6cfb1",
+ "displayName": "Decrease volume",
+ "name": "decreaseVolume",
+ "paramTypes": [
+ {
+ "id": "736f5e25-31ce-4d17-80ab-b82b18cf6540",
+ "name": "step",
+ "displayName": "Step size",
+ "type": "uint",
+ "minValue": 1,
+ "maxValue": 5,
+ "defaultValue": 5
+ }
+ ]
+ },
+ {
+ "id": "9d1812ba-77a6-493a-8fd0-77a6c44d3765",
+ "name": "skipBack",
+ "displayName": "Skip back"
+ },
+ {
+ "id": "c5a8d2e0-60d8-4092-ae77-697b13ce5946",
+ "name": "stop",
+ "displayName": "Stop"
+ },
+ {
+ "id": "18e999ae-a1ef-476e-a416-05b8b07527ae",
+ "name": "play",
+ "displayName": "Play"
+ },
+ {
+ "id": "9f019a46-821a-4069-9772-fc649c39f941",
+ "name": "pause",
+ "displayName": "Pause"
+ },
+ {
+ "id": "ffceeb31-8b66-4d03-b664-57a3f9c9ec1a",
+ "name": "skipNext",
+ "displayName": "Skip next"
+ }
+ ],
+ "browserItemActionTypes": [
+ {
+ "id": "24e8955e-4a51-411b-863d-aa70c2ab90ef",
+ "name": "playRandom",
+ "displayName": "Play random album"
+ }
+ ]
+ },
+ {
+ "id": "664ba198-a3e2-4380-83f3-adfb22a13130",
+ "name": "zone",
+ "displayName": "Yamaha AV Receiver Zone",
+ "createMethods": ["discovery"],
+ "interfaces": ["mediaplayer", "mediacontroller", "volumecontroller", "mediametadataprovider", "connectable", "power"],
+ "browsable": true,
+ "paramTypes": [
+ {
+ "id": "9abc9961-0582-4b31-8f28-6659a3434352",
+ "name": "serial",
+ "displayName": "Serial Number",
+ "type" : "QString",
+ "readOnly": true
+ },
+ {
+ "id": "6cfafac2-4aa5-4e73-b06e-550f4f66bc6a",
+ "name": "zoneId",
+ "displayName": "Zone number",
+ "type" : "int",
+ "readOnly": true
+ }
+ ],
+ "settingsTypes": [
+
+ ],
+ "stateTypes": [
+ {
+ "id": "30c97665-1843-46d5-9091-06d97f456827",
+ "name": "connected",
+ "displayName": "Connected",
+ "displayNameEvent": "Connected changed",
+ "defaultValue": false,
+ "type": "bool",
+ "cached": false
+ },
+ {
+ "displayName": "Power",
+ "id": "bcace3de-902e-4361-8be3-68e0e42cb729",
+ "name": "power",
+ "displayNameEvent": "Power changed",
+ "displayNameAction": "Set power",
+ "type": "bool",
+ "defaultValue": false,
+ "writable": true
+ },
+ {
+ "displayName": "Sleep",
+ "id": "7ac5101f-96cd-40f4-a565-a15c266dafdb",
+ "name": "sleep",
+ "displayNameEvent": "Sleep timer changed",
+ "displayNameAction": "Set sleep timer",
+ "type": "QString",
+ "writable": true,
+ "possibleValues": [
+ "Off",
+ "30 min",
+ "60 min",
+ "90 min",
+ "120 min"
+ ],
+ "defaultValue": "Off"
+ },
+ {
+ "displayName": "Mute",
+ "id": "26ac1df0-77c7-4461-95a5-294ce629ad5a",
+ "name": "mute",
+ "displayNameEvent": "Mute changed",
+ "displayNameAction": "Set mute",
+ "type": "bool",
+ "defaultValue": false,
+ "writable": true
+ },
+ {
+ "displayName": "Volume",
+ "id": "090ade12-ade2-43df-9d47-3d0cfc7c1def",
+ "name": "volume",
+ "displayNameEvent": "Volume changed",
+ "displayNameAction": "Set volume",
+ "type": "int",
+ "defaultValue": -500,
+ "minValue": -800,
+ "maxValue": -100,
+ "writable": true
+ },
+ {
+ "displayName": "Input Source",
+ "id": "40de91af-6c09-4a72-8812-bdff354917d9",
+ "name": "inputSource",
+ "displayNameEvent": "Input source changed",
+ "displayNameAction": "Set input source",
+ "type": "QString",
+ "writable": true,
+ "possibleValues": [
+ "TUNER",
+ "NAPSTER",
+ "SPOTIFY",
+ "SERVER",
+ "AirPlay",
+ "NET RADIO",
+ "USB",
+ "AV5",
+ "AV6",
+ "AUDIO1",
+ "AUDIO2"
+ ],
+ "defaultValue": "SERVER"
+ },
+ {
+ "id": "14288bcb-be26-43a9-895a-f519fea47e62",
+ "name": "artist",
+ "displayName": "Artist",
+ "displayNameEvent": "Artist changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "330e5570-586c-44bb-87ee-58d37d139985",
+ "name": "collection",
+ "displayName": "Album",
+ "displayNameEvent": "Album changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "dfb1fac7-8a2e-42d3-b2f3-badb1c80be5c",
+ "name": "title",
+ "displayName": "Title",
+ "displayNameEvent": "Title changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "5bb9d082-4c22-49c1-8b03-94aab00b0b97",
+ "name": "artwork",
+ "displayName": "Artwork",
+ "displayNameEvent": "Artwork changed",
+ "type": "QString",
+ "defaultValue": ""
+ },
+ {
+ "id": "eca64246-b1a6-4204-b0ce-fc800b8135d7",
+ "name": "playerType",
+ "displayName": "Player type",
+ "displayNameEvent": "Player type changed",
+ "possibleValues": [
+ "audio",
+ "video"
+ ],
+ "type": "QString",
+ "defaultValue": "audio"
+ },
+ {
+ "id": "03a1005e-91be-4c09-869d-4a90ba7c6fca",
+ "name": "playbackStatus",
+ "displayName": "Playback status",
+ "displayNameEvent": "Playback status changed",
+ "displayNameAction": "Set playback status",
+ "type": "QString",
+ "defaultValue": "Stopped",
+ "possibleValues": ["Playing", "Paused", "Stopped"],
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "7444ed1e-952c-4d33-9673-bcc599ffb52a",
+ "name": "shuffle",
+ "displayName": "Shuffle",
+ "displayNameEvent": "Shuffle changed",
+ "displayNameAction": "Set shuffle",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false,
+ "writable": true
+ },
+ {
+ "id": "d03b4144-46fe-47ba-a287-3408b1dc3c54",
+ "name": "repeat",
+ "displayName": "Repeat mode",
+ "displayNameEvent": "Repeat mode changed",
+ "displayNameAction": "Set repeat mode",
+ "type": "QString",
+ "defaultValue": "None",
+ "possibleValues": ["None", "One", "All"],
+ "cached": false,
+ "writable": true
+ }
+ ],
+ "actionTypes": [
+ {
+ "id": "7ffbe00e-b5e6-4602-9c1c-7319c0fc3257",
+ "name": "randomAlbum",
+ "displayName": "Play Random Album"
+ },
+ {
+ "id": "ed011bda-7b52-4ba1-a99e-60ddf31bc859",
+ "displayName": "Increase volume",
+ "name": "increaseVolume",
+ "paramTypes": [
+ {
+ "id": "065d7fc4-5819-49f6-917e-18fbef84e24e",
+ "name": "step",
+ "displayName": "Step size",
+ "type": "uint",
+ "minValue": 1,
+ "maxValue": 5,
+ "defaultValue": 5
+ }
+ ]
+ },
+ {
+ "id": "e753b540-c7d3-4f54-8078-e261ebf0bcbe",
+ "displayName": "Decrease volume",
+ "name": "decreaseVolume",
+ "paramTypes": [
+ {
+ "id": "e5ee7aaa-ed67-4807-8941-0aa8266c4951",
+ "name": "step",
+ "displayName": "Step size",
+ "type": "uint",
+ "minValue": 1,
+ "maxValue": 5,
+ "defaultValue": 5
+ }
+ ]
+ },
+ {
+ "id": "ac22babd-b72a-48bb-913a-e47982c5bff9",
+ "name": "skipBack",
+ "displayName": "Skip back"
+ },
+ {
+ "id": "d8eab201-27bc-4850-bbaa-be91e58cd874",
+ "name": "stop",
+ "displayName": "Stop"
+ },
+ {
+ "id": "fe30284b-6863-4319-b71e-db18c0b1e2f3",
+ "name": "play",
+ "displayName": "Play"
+ },
+ {
+ "id": "2568ac35-d051-442f-877d-a359129b3164",
+ "name": "pause",
+ "displayName": "Pause"
+ },
+ {
+ "id": "e691da7c-ed1d-4b09-b60a-41a7fcd89e49",
+ "name": "skipNext",
+ "displayName": "Skip next"
+ }
+ ],
+ "browserItemActionTypes": [
+ {
+ "id": "161ae3ac-efb1-460e-9564-c175bbf68861",
+ "name": "playRandom",
+ "displayName": "Play random album"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/yamahaavr/integrationpluginyamaha.py b/yamahaavr/integrationpluginyamaha.py
new file mode 100644
index 00000000..f3685ead
--- /dev/null
+++ b/yamahaavr/integrationpluginyamaha.py
@@ -0,0 +1,1382 @@
+import nymea
+import time
+import threading
+import json
+import requests
+import random
+from xml.sax.saxutils import unescape
+from zeroconf import IPVersion, ServiceBrowser, ServiceInfo, Zeroconf
+from typing import Callable, List
+
+class ZeroconfDevice(object):
+ # To do: replace with nymea serviceBrowser
+ def __init__(self, name: str, ip: str, port: int, model: str, id: str) -> None:
+ self.name = name
+ self.ip = ip
+ self.port = port
+ self.model = model
+ self.id = id
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({self.__dict__})"
+
+ def __eq__(self, other) -> bool:
+ return self is other or self.__dict__ == other.__dict__
+
+class ZeroconfListener(object):
+ # To do: replace with nymea serviceBrowser
+ """Basic zeroconf listener."""
+
+ def __init__(self, func: Callable[[ServiceInfo], None]) -> None:
+ """Initialize zeroconf listener with function callback."""
+ self._func = func
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({self.__dict__})"
+
+ def __eq__(self, other) -> bool:
+ return self is other or self.__dict__ == other.__dict__
+
+ def add_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
+ """Callback function when zeroconf service is discovered."""
+ self._func(zeroconf.get_service_info(type, name))
+
+ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
+ return
+
+ def remove_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
+ return
+
+pollTimer = None
+
+playPoll = False
+
+def discoverThings(info):
+ if info.thingClassId == receiverThingClassId:
+ logger.log("Discovery started for", info.thingClassId)
+ discoveredIps = findIps()
+
+ for i in range(0, len(discoveredIps)):
+ deviceIp = discoveredIps[i]
+ rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
+ body = 'GetParam'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ rr = requests.post(rUrl, headers=headers, data=body)
+ pollResponse = rr.text
+ if rr.status_code == requests.codes.ok:
+ logger.log("Device with IP " + deviceIp + " is a supported Yamaha AVR.")
+ # get device info
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+11:stringIndex2]
+ systemId = responseExtract
+ logger.log("System ID:", systemId)
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+12:stringIndex2]
+ modelType = "Yamaha " + responseExtract
+ # check if device already known
+ exists = False
+ for thing in myThings():
+ logger.log("Comparing to existing receivers: is %s a receiver?" % (thing.name))
+ if thing.thingClassId == receiverThingClassId:
+ logger.log("Yes, %s is a receiver." % (thing.name))
+ if thing.paramValue(receiverThingSerialParamTypeId) == systemId:
+ logger.log("Already have receiver with serial number %s in the system: %s" % (systemId, thing.name))
+ exists = True
+ else:
+ logger.log("Thing %s doesn't match with found receiver with serial number %s" % (thing.name, systemId))
+ if exists == False: # Receiver doesn't exist yet, so add it
+ thingDescriptor = nymea.ThingDescriptor(receiverThingClassId, modelType)
+ thingDescriptor.params = [
+ nymea.Param(receiverThingSerialParamTypeId, systemId)
+ ]
+ info.addDescriptor(thingDescriptor)
+ else: # Receiver already exists, so show it to allow reconfiguration
+ thingDescriptor = nymea.ThingDescriptor(receiverThingClassId, modelType, thingId=thing.id)
+ thingDescriptor.params = [
+ nymea.Param(receiverThingSerialParamTypeId, systemId)
+ ]
+ info.addDescriptor(thingDescriptor)
+ else:
+ logger.log("Device with IP " + deviceIp + " does not appear to be a supported Yamaha AVR.")
+ info.finish(nymea.ThingErrorNoError)
+ return
+
+ if info.thingClassId == zoneThingClassId:
+ logger.log("Discovery started for", info.thingClassId)
+
+ for possibleReceiver in myThings():
+ logger.log("Looking for existing receivers to add zones: is %s a receiver?" % (possibleReceiver.name))
+ if possibleReceiver.thingClassId == receiverThingClassId:
+ receiver = possibleReceiver
+ deviceIp = receiver.stateValue(receiverUrlStateTypeId)
+ logger.log("Yes, %s with IP address %s is a receiver, looking for zones." % (receiver.name, deviceIp))
+ rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
+ body = 'GetParam'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ rr = requests.post(rUrl, headers=headers, data=body)
+ pollResponse = rr.text
+ possibleZones = list(("Zone_2", "Zone_3", "Zone_4"))
+
+ for zone in possibleZones:
+ stringIndex1 = pollResponse.find("<" + zone + ">")
+ stringIndex2 = pollResponse.find("" + zone + ">")
+ zoneFound = int(pollResponse[stringIndex1+8:stringIndex2])
+ zoneNbr = int(zone[5:6])
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ systemId = pollResponse[stringIndex1+11:stringIndex2]
+ if zoneFound == 1:
+ logger.log("Additional zone with number %s found." % (str(zoneNbr)))
+ # test if zone already exists
+ exists = False
+ for possibleZone in myThings():
+ logger.log("Comparing to existing zones: is %s a zone?" % (possibleZone.name))
+ if possibleZone.thingClassId == zoneThingClassId:
+ zone = possibleZone
+ logger.log("Yes, %s is a zone." % (possibleZone.name))
+ if zone.paramValue(zoneThingSerialParamTypeId) == systemId and zone.paramValue(zoneThingZoneIdParamTypeId) == zoneNbr:
+ logger.log("Already have zone with number %s in the system" % (str(zoneNbr)))
+ exists = True
+ else:
+ logger.log("Thing %s doesn't match with found zone with number %s" % (possibleZone.name, str(zoneNbr)))
+ elif possibleZone.thingClassId == receiverThingClassId:
+ logger.log("Yes, %s is a main zone." % (possibleZone.name))
+ else:
+ logger.log("No, %s is not a zone." % (possibleZone.name))
+ if exists == False: # Zone doesn't exist yet, so add it
+ zoneName = receiver.name + " Zone " + str(zoneNbr)
+ logger.log("Found new additional zone:", zone, zoneNbr)
+ logger.log("Adding %s to the system with parent:" % (zoneName), receiver.name, receiver.id)
+ thingDescriptor = nymea.ThingDescriptor(zoneThingClassId, zoneName, parentId=receiver.id)
+ thingDescriptor.params = [
+ nymea.Param(zoneThingSerialParamTypeId, systemId),
+ nymea.Param(zoneThingZoneIdParamTypeId, zoneNbr)
+ ]
+ info.addDescriptor(thingDescriptor)
+ else: # Zone already exists, so show it to allow reconfiguration
+ zoneName = receiver.name + " Zone " + str(zoneNbr)
+ thingDescriptor = nymea.ThingDescriptor(zoneThingClassId, zoneName, thingId=zone.id, parentId=receiver.id)
+ thingDescriptor.params = [
+ nymea.Param(zoneThingSerialParamTypeId, systemId),
+ nymea.Param(zoneThingZoneIdParamTypeId, zoneNbr)
+ ]
+ info.addDescriptor(thingDescriptor)
+ info.finish(nymea.ThingErrorNoError)
+ return
+
+def findIps():
+ # To do: in future use nymea capabilities:
+ # no need of any external libraries, you can just call "serviceBrowser = hardwareManager.zeroconf.registerServiceBrowser()"
+ # and can then loop over "serviceBrowser.entries"# serviceBrowser = hardwareManager.zeroconf.registerServiceBrowser()
+ # for i in range(0, len(serviceBrowser.entries)):
+ # logger.log(serviceBrowser.entries[i])
+
+ # foreach (const ZeroConfServiceEntry &entry, m_serviceBrowser->serviceEntries()) {
+ # if (entry.hostAddress().protocol() == QAbstractSocket::IPv6Protocol && entry.hostAddress().toString().startsWith("fe80")) {
+ # // We don't support link-local ipv6 addresses yet. skip those entries
+ # continue;
+ # }
+ # QString uuid;
+ # foreach (const QString &txt, entry.txt()) {
+ # if (txt.startsWith("uuid")) {
+ # uuid = txt.split("=").last();
+ # break;
+ # }
+ # }
+ # if (QUuid(uuid) == kodiUuid) {
+ # ipString = entry.hostAddress().toString();
+ # port = entry.port();
+ # break;
+ # }
+ # }
+ # for now we use zeroconf (def discover & classes ZeroconfDevice & ZeroconfListener) as borrowed from pyvizio
+
+ ipList = discover("_http._tcp.local.", 5)
+ logger.log(ipList)
+
+ discoveredIps = []
+ for i in range(0, len(ipList)):
+ deviceInfo = ipList[i]
+ if "Yamaha" in deviceInfo.name:
+ discoveredIps.append(deviceInfo.ip)
+ return discoveredIps
+
+def discover(service_type: str, timeout: int = 5) -> List[ZeroconfDevice]:
+ # To do: replace with nymea serviceBrowser
+ """From pyvizio: Return all discovered zeroconf services of a given service type over given timeout period."""
+ services = []
+
+ def append_service(info: ServiceInfo) -> None:
+ """Append discovered zeroconf service to service list."""
+ name = info.name[: -(len(info.type) + 1)]
+ ip = info.parsed_addresses(IPVersion.V4Only)[0]
+ port = info.port
+ model = info.properties.get(b"name", "")
+ id = info.properties.get(b"id")
+
+ # handle id decode for various discovered use cases
+ if isinstance(id, bytes):
+ try:
+ int(id, 16)
+ except Exception:
+ id = id.hex()
+ else:
+ id = None
+
+ service = ZeroconfDevice(name, ip, port, model, id)
+ services.append(service)
+
+ zeroconf = Zeroconf()
+ ServiceBrowser(zeroconf, service_type, ZeroconfListener(append_service))
+ time.sleep(timeout)
+ zeroconf.close()
+
+ return services
+
+def setupThing(info):
+ if info.thing.thingClassId == receiverThingClassId:
+ searchSystemId = info.thing.paramValue(receiverThingSerialParamTypeId)
+ logger.log("setupThing called for", info.thing.name, searchSystemId)
+
+ discoveredIps = findIps()
+ found = False
+
+ for i in range(0, len(discoveredIps)):
+ deviceIp = discoveredIps[i]
+ rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
+ body = 'GetParam'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ rr = requests.post(rUrl, headers=headers, data=body)
+ pollResponse = rr.text
+ if rr.status_code == requests.codes.ok:
+ logger.log("Device with IP " + deviceIp + " is a supported Yamaha AVR.")
+ # get device info
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+11:stringIndex2]
+ systemId = responseExtract
+ logger.log("System ID:", systemId)
+ # check if this is the device with the serial number we're looking for
+ if systemId == searchSystemId:
+ logger.log("Device with IP " + deviceIp + " is the existing device.")
+ found = True
+ info.thing.setStateValue(receiverUrlStateTypeId, deviceIp)
+ rr2 = rr
+ else:
+ logger.log("Device with IP " + deviceIp + " does not appear to be a supported Yamaha AVR.")
+ if found == True:
+ info.thing.setStateValue(receiverConnectedStateTypeId, True)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ else:
+ info.thing.setStateValue(receiverConnectedStateTypeId, False)
+ info.finish(nymea.ThingErrorHardwareFailure, "Error connecting to the device in the network.")
+
+ logger.log("Receiver added:", info.thing.name)
+
+ # If no poll timer is set up yet, start it now
+ logger.log("Creating pollService")
+ global pollTimer
+ if pollTimer == None:
+ logger.log("Starting timer @ setupThing")
+ pollTimer = threading.Timer(30, pollService)
+ pollTimer.start()
+ else:
+ logger.log("Timer already exists @ setupThing")
+
+ info.finish(nymea.ThingErrorNoError)
+ return
+
+ # Setup for the zone
+ if info.thing.thingClassId == zoneThingClassId:
+ logger.log("SetupThing for zone:", info.thing.name)
+ # get parent receiver thing, needed to get deviceIp
+ for possibleParent in myThings():
+ if possibleParent.id == info.thing.parentId:
+ parentReceiver = possibleParent
+ deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId)
+ zoneId = info.thing.paramValue(zoneThingZoneIdParamTypeId)
+ zone = "Zone_" + str(zoneId)
+ try:
+ pollReceiver(info.thing)
+ logger.log(zone + " added.")
+ info.thing.setStateValue(zoneConnectedStateTypeId, True)
+ except:
+ logger.warn("Error getting zone state");
+ info.finish(nymea.ThingErrorHardwareFailure, "Unable to set up zone.")
+ info.thing.setStateValue(zoneConnectedStateTypeId, False)
+ return;
+
+ # set up polling for zone status
+ info.finish(nymea.ThingErrorNoError)
+ return
+
+def pollReceiver(info):
+ global playPoll
+ if info.thingClassId == zoneThingClassId:
+ # get parent receiver thing, needed to get deviceIp
+ for possibleParent in myThings():
+ if possibleParent.id == info.parentId:
+ parentReceiver = possibleParent
+ deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId)
+ zoneId = info.paramValue(zoneThingZoneIdParamTypeId)
+ logger.log("polling zone", deviceIp, info.name)
+ bodyStart = ''
+ bodyEnd = ''
+ elif info.thingClassId == receiverThingClassId:
+ deviceIp = info.stateValue(receiverUrlStateTypeId)
+ logger.log("polling receiver", deviceIp, info.name)
+ bodyStart = ''
+ bodyEnd = ''
+ rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
+ body = bodyStart + 'GetParam' + bodyEnd
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ pr = requests.post(rUrl, headers=headers, data=body)
+ pollResponse = pr.text
+ # add distinction between receiver & zone
+ if info.thingClassId == receiverThingClassId:
+ receiver = info
+ if pr.status_code == requests.codes.ok:
+ receiver.setStateValue(receiverConnectedStateTypeId, True)
+ # Get power state
+ if pollResponse.find("Standby") != -1:
+ receiver.setStateValue(receiverPowerStateTypeId, False)
+ powerState = False
+ elif pollResponse.find("On") != -1:
+ receiver.setStateValue(receiverPowerStateTypeId, True)
+ powerState = True
+ else:
+ logger.log("Power state not found!")
+ # Get sleep state
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+7:stringIndex2]
+ receiver.setStateValue(receiverSleepStateTypeId, responseExtract)
+ # Get mute state
+ if pollResponse.find("Off") != -1:
+ receiver.setStateValue(receiverMuteStateTypeId, False)
+ elif pollResponse.find("On") != -1:
+ receiver.setStateValue(receiverMuteStateTypeId, True)
+ else:
+ logger.log("Mute state not found!")
+ # Get pure direct state
+ if pollResponse.find("Off") != -1:
+ receiver.setStateValue(receiverPureDirectStateTypeId, False)
+ elif pollResponse.find("On") != -1:
+ receiver.setStateValue(receiverPureDirectStateTypeId, True)
+ else:
+ logger.log("Pure Direct state not found!")
+ # Get enhancer state
+ if pollResponse.find("Off") != -1:
+ receiver.setStateValue(receiverEnhancerStateTypeId, False)
+ elif pollResponse.find("On") != -1:
+ receiver.setStateValue(receiverEnhancerStateTypeId, True)
+ else:
+ logger.log("Enhancer state not found!")
+ # Get input
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ inputSource = pollResponse[stringIndex1+18:stringIndex2]
+ receiver.setStateValue(receiverInputSourceStateTypeId, inputSource)
+ videoSources = ["HDMI1","HDMI2","HDMI3","HDMI4","HDMI5","AV1","AV2","AV3","AV4","AV5","AV6","V-AUX"]
+ if inputSource in videoSources:
+ receiver.setStateValue(receiverPlayerTypeStateTypeId, "video")
+ else:
+ receiver.setStateValue(receiverPlayerTypeStateTypeId, "audio")
+ # Get sound program
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+15:stringIndex2]
+ receiver.setStateValue(receiverSurroundModeStateTypeId, responseExtract)
+ # Get Cinema DSP 3D state
+ stringIndex1 = pollResponse.find("<_3D_Cinema_DSP>")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+16:stringIndex2]
+ receiver.setStateValue(receiverCinemaDSP3DStateTypeId, responseExtract)
+ # Get Adaptive DRC state
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+14:stringIndex2]
+ receiver.setStateValue(receiverAdaptiveDRCStateTypeId, responseExtract)
+ # Get volume - volume is represented by int in Yamaha API, but shown as double = int/10 in Yamaha UI - this is ignored here as nymea wants volume to be an int
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+18:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ volume = int(responseExtract)
+ receiver.setStateValue(receiverVolumeStateTypeId, volume)
+ # Get bass
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+11:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ bass = int(responseExtract)
+ receiver.setStateValue(receiverBassStateTypeId, bass)
+ # Get treble
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+13:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ treble = int(responseExtract)
+ receiver.setStateValue(receiverTrebleStateTypeId, treble)
+ # Get dialogue level
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+14:stringIndex2]
+ dialogueLvl = int(responseExtract)
+ receiver.setStateValue(receiverDialogueLevelStateTypeId, dialogueLvl)
+ # Get dialogue lift
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+15:stringIndex2]
+ dialogueLift = int(responseExtract)
+ receiver.setStateValue(receiverDialogueLiftStateTypeId, dialogueLift)
+ # Get subwoofer trim
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+21:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ subTrim = int(responseExtract)
+ receiver.setStateValue(receiverSubwooferTrimStateTypeId, subTrim)
+ # Get player info
+ body = '<' + inputSource + '>GetParam' + inputSource + '>'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ plr = requests.post(rUrl, headers=headers, data=body)
+ if plr.status_code == requests.codes.ok and powerState == True:
+ playerResponse = plr.text
+ # Get repeat state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+8:stringIndex2]
+ if responseExtract not in ["None", "One", "All"]:
+ responseExtract = "None"
+ receiver.setStateValue(receiverRepeatStateTypeId, responseExtract)
+ # Get shuffle state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+9:stringIndex2]
+ if responseExtract == "On":
+ shuffleStatus = True
+ else:
+ shuffleStatus = False
+ receiver.setStateValue(receiverShuffleStateTypeId, shuffleStatus)
+ # Get playback state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+15:stringIndex2]
+ if responseExtract == "Play":
+ playStatus = "Playing"
+ playPoll = True or playPoll
+ elif responseExtract == "Pause":
+ playStatus = "Paused"
+ playPoll = True or playPoll
+ else:
+ playStatus = "Stopped"
+ playPoll = False or playPoll
+ receiver.setStateValue(receiverPlaybackStatusStateTypeId, playStatus)
+ # Get meta info
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+8:stringIndex2]
+ receiver.setStateValue(receiverArtistStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+7:stringIndex2]
+ receiver.setStateValue(receiverCollectionStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+6:stringIndex2]
+ receiver.setStateValue(receiverTitleStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
+ # Get artwork --> Yamaha artwork file type isn't recognized by nymea: browse for external cover art?
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+5:stringIndex2]
+ artURL = 'http://' + deviceIp + ':80' + responseExtract
+ receiver.setStateValue(receiverArtworkStateTypeId, artURL)
+ else:
+ # Playing from external source so no info available
+ receiver.setStateValue(receiverRepeatStateTypeId, "None")
+ receiver.setStateValue(receiverShuffleStateTypeId, False)
+ receiver.setStateValue(receiverPlaybackStatusStateTypeId, "Stopped")
+ receiver.setStateValue(receiverArtistStateTypeId, "")
+ receiver.setStateValue(receiverCollectionStateTypeId, "")
+ receiver.setStateValue(receiverTitleStateTypeId, "")
+ receiver.setStateValue(receiverArtworkStateTypeId, "")
+ else:
+ receiver.setStateValue(receiverConnectedStateTypeId, False)
+ elif info.thingClassId == zoneThingClassId:
+ zone = info
+ if pr.status_code == requests.codes.ok:
+ zone.setStateValue(zoneConnectedStateTypeId, True)
+ # Get power state
+ if pollResponse.find("Standby") != -1:
+ zone.setStateValue(zonePowerStateTypeId, False)
+ powerState = False
+ elif pollResponse.find("On") != -1:
+ zone.setStateValue(zonePowerStateTypeId, True)
+ powerState = True
+ else:
+ logger.log("Power state not found!")
+ # Get sleep state
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+7:stringIndex2]
+ zone.setStateValue(zoneSleepStateTypeId, responseExtract)
+ # Get mute state
+ if pollResponse.find("Off") != -1:
+ zone.setStateValue(zoneMuteStateTypeId, False)
+ elif pollResponse.find("On") != -1:
+ zone.setStateValue(zoneMuteStateTypeId, True)
+ else:
+ logger.log("Mute state not found!")
+ # Get input
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ inputSource = pollResponse[stringIndex1+18:stringIndex2]
+ zone.setStateValue(zoneInputSourceStateTypeId, inputSource)
+ videoSources = ["HDMI1","HDMI2","HDMI3","HDMI4","HDMI5","AV1","AV2","AV3","AV4","AV5","AV6","V-AUX"]
+ if inputSource in videoSources:
+ zone.setStateValue(zonePlayerTypeStateTypeId, "video")
+ else:
+ zone.setStateValue(zonePlayerTypeStateTypeId, "audio")
+ # Get volume
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+18:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ volume = int(responseExtract)
+ zone.setStateValue(zoneVolumeStateTypeId, volume)
+ # Get player info
+ body = '<' + inputSource + '>GetParam' + inputSource + '>'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ plr = requests.post(rUrl, headers=headers, data=body)
+ if plr.status_code == requests.codes.ok and powerState == True:
+ playerResponse = plr.text
+ # Get repeat state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+8:stringIndex2]
+ if responseExtract not in ["None", "One", "All"]:
+ responseExtract = "None"
+ zone.setStateValue(zoneRepeatStateTypeId, responseExtract)
+ # Get shuffle state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+9:stringIndex2]
+ if responseExtract == "On":
+ shuffleStatus = True
+ else:
+ shuffleStatus = False
+ zone.setStateValue(zoneShuffleStateTypeId, shuffleStatus)
+ # Get playback state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+15:stringIndex2]
+ if responseExtract == "Play":
+ playStatus = "Playing"
+ playPoll = True or playPoll
+ elif responseExtract == "Pause":
+ playStatus = "Paused"
+ playPoll = True or playPoll
+ else:
+ playStatus = "Stopped"
+ playPoll = False or playPoll
+ zone.setStateValue(zonePlaybackStatusStateTypeId, playStatus)
+ # Get meta info
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+8:stringIndex2]
+ zone.setStateValue(zoneArtistStateTypeId, responseExtract)
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+7:stringIndex2]
+ zone.setStateValue(zoneCollectionStateTypeId, responseExtract)
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+6:stringIndex2]
+ zone.setStateValue(zoneTitleStateTypeId, responseExtract)
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+5:stringIndex2]
+ artURL = 'http://' + deviceIp + ':80' + responseExtract
+ zone.setStateValue(zoneArtworkStateTypeId, artURL)
+ else:
+ # Playing from external source so no info available
+ zone.setStateValue(zoneRepeatStateTypeId, "None")
+ zone.setStateValue(zoneShuffleStateTypeId, False)
+ zone.setStateValue(zonePlaybackStatusStateTypeId, "Stopped")
+ zone.setStateValue(zoneArtistStateTypeId, "")
+ zone.setStateValue(zoneCollectionStateTypeId, "")
+ zone.setStateValue(zoneTitleStateTypeId, "")
+ zone.setStateValue(zoneArtworkStateTypeId, "")
+ else:
+ zone.setStateValue(zoneConnectedStateTypeId, False)
+
+def pollService():
+ logger.log("pollService!!!")
+ global pollTimer
+ global playPoll
+ # restart the timer for next poll (if player was playing at previous poll, increase poll frequency)
+ # we start the timer before polling the receivers to test if this avoids the timer not restarting due to request errors in pollReceiver
+ if playPoll == True:
+ interval = 10
+ else:
+ interval = 30
+ logger.log("Restarting timer @ pollService")
+ pollTimer = threading.Timer(interval, pollService)
+ pollTimer.start()
+ playPoll = False
+ # Poll all receivers we know
+ for thing in myThings():
+ if thing.thingClassId == receiverThingClassId or thing.thingClassId == zoneThingClassId:
+ pollReceiver(thing)
+
+def executeAction(info):
+ pollReceiver(info.thing)
+ if info.thing.thingClassId == zoneThingClassId:
+ # get parent receiver thing, needed to get deviceIp
+ for possibleParent in myThings():
+ if possibleParent.id == info.thing.parentId:
+ parentReceiver = possibleParent
+ deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId)
+ zoneId = info.thing.paramValue(zoneThingZoneIdParamTypeId)
+ bodyStart = ''
+ bodyEnd = ''
+ source = info.thing.stateValue(zoneInputSourceStateTypeId)
+ powerCheck = info.thing.stateValue(zonePowerStateTypeId)
+ elif info.thing.thingClassId == receiverThingClassId:
+ deviceIp = info.thing.stateValue(receiverUrlStateTypeId)
+ bodyStart = ''
+ bodyEnd = ''
+ source = info.thing.stateValue(receiverInputSourceStateTypeId)
+ powerCheck = info.thing.stateValue(receiverPowerStateTypeId)
+
+ logger.log("executeAction called for thing", info.thing.name, deviceIp, source, info.actionTypeId, info.params)
+ rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+
+ # turn receiver/zone on if needed, before executing the action
+ if powerCheck == False and info.actionTypeId != receiverPowerActionTypeId and info.actionTypeId != zonePowerActionTypeId:
+ body = bodyStart + 'On' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+
+ if info.actionTypeId == receiverIncreaseVolumeActionTypeId or info.actionTypeId == zoneIncreaseVolumeActionTypeId:
+ if info.actionTypeId == receiverIncreaseVolumeActionTypeId:
+ stepsize = info.paramValue(receiverIncreaseVolumeActionStepParamTypeId)
+ else:
+ stepsize = info.paramValue(zoneIncreaseVolumeActionStepParamTypeId)
+ volumeDelta = stepsize * 10
+ while abs(volumeDelta) >= 5:
+ if volumeDelta >= 50:
+ step = "Up 5 dB"
+ volumeDelta -= 50
+ elif volumeDelta >= 10:
+ step = "Up 1 dB"
+ volumeDelta -= 10
+ elif volumeDelta >= 5:
+ step = "Up"
+ volumeDelta -= 5
+ else:
+ break
+ body = bodyStart + '' + step + '' + bodyEnd
+ pr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverDecreaseVolumeActionTypeId or info.actionTypeId == zoneDecreaseVolumeActionTypeId:
+ if info.actionTypeId == receiverDecreaseVolumeActionTypeId:
+ stepsize = info.paramValue(receiverDecreaseVolumeActionStepParamTypeId)
+ else:
+ stepsize = info.paramValue(zoneDecreaseVolumeActionStepParamTypeId)
+ volumeDelta = stepsize * -10
+ while abs(volumeDelta) >= 5:
+ if volumeDelta <= -50:
+ step = "Down 5 dB"
+ volumeDelta += 50
+ elif volumeDelta <= -10:
+ step = "Down 1 dB"
+ volumeDelta += 10
+ elif volumeDelta <= -5:
+ step = "Down"
+ volumeDelta += 5
+ else:
+ break
+ body = bodyStart + '' + step + '' + bodyEnd
+ pr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverSkipBackActionTypeId or info.actionTypeId == zoneSkipBackActionTypeId:
+ body = '<' + source + '>Skip Rev' + source + '>'
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ # AirPlay statusupdates appear to take a while longer to be available in API
+ if source == "AirPlay":
+ time.sleep(6)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverStopActionTypeId or info.actionTypeId == zoneStopActionTypeId:
+ body = '<' + source + '>Stop' + source + '>'
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ if source == "AirPlay":
+ time.sleep(6)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverPlayActionTypeId or info.actionTypeId == zonePlayActionTypeId:
+ body = '<' + source + '>Play' + source + '>'
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ if source == "AirPlay":
+ time.sleep(6)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverPauseActionTypeId or info.actionTypeId == zonePauseActionTypeId:
+ body = '<' + source + '>Pause' + source + '>'
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ if source == "AirPlay":
+ time.sleep(6)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverSkipNextActionTypeId or info.actionTypeId == zoneSkipNextActionTypeId:
+ body = '<' + source + '>Skip Fwd' + source + '>'
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ if source == "AirPlay":
+ time.sleep(6)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverPowerActionTypeId or info.actionTypeId == zonePowerActionTypeId:
+ if info.actionTypeId == receiverPowerActionTypeId:
+ power = info.paramValue(receiverPowerActionPowerParamTypeId)
+ else:
+ power = info.paramValue(zonePowerActionPowerParamTypeId)
+ if power == True:
+ powerString = "On"
+ else:
+ powerString = "Standby"
+ body = bodyStart + '' + powerString + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverSleepActionTypeId or info.actionTypeId == zoneSleepActionTypeId:
+ if info.actionTypeId == receiverSleepActionTypeId:
+ sleepString = info.paramValue(receiverSleepActionSleepParamTypeId)
+ else:
+ sleepString = info.paramValue(zoneSleepActionSleepParamTypeId)
+ body = bodyStart + '' + sleepString + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverMuteActionTypeId or info.actionTypeId == zoneMuteActionTypeId:
+ if info.actionTypeId == receiverMuteActionTypeId:
+ mute = info.paramValue(receiverMuteActionMuteParamTypeId)
+ else:
+ mute = info.paramValue(zoneMuteActionMuteParamTypeId)
+ if mute == True:
+ muteString = "On"
+ else:
+ muteString = "Off"
+ body = bodyStart + '' + muteString + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverVolumeActionTypeId or info.actionTypeId == zoneVolumeActionTypeId:
+ if info.actionTypeId == receiverVolumeActionTypeId:
+ newVolume = info.paramValue(receiverVolumeActionVolumeParamTypeId)
+ else:
+ newVolume = info.paramValue(zoneVolumeActionVolumeParamTypeId)
+ # volume needs to be multiple of 5
+ remainder = newVolume % 5
+ newVolume -= remainder
+ volumeString = str(newVolume)
+ logger.log("Volume set to", newVolume)
+ body = bodyStart + '' + volumeString + '1dB' + bodyEnd
+ pr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverSubwooferTrimActionTypeId:
+ newTrim = info.paramValue(receiverSubwooferTrimActionSubwooferTrimParamTypeId)
+ # trim needs to be multiple of 5
+ remainder = newTrim % 5
+ newTrim -= remainder
+ trimString = str(newTrim)
+ logger.log("Subwoofer trim set to", newTrim)
+ body = bodyStart + '' + trimString + '1dB' + bodyEnd
+ pr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverPureDirectActionTypeId:
+ pureDirect = info.paramValue(receiverPureDirectActionPureDirectParamTypeId)
+ if pureDirect == True:
+ PureDirectString = "On"
+ else:
+ PureDirectString = "Off"
+ body = bodyStart + '' + PureDirectString + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverEnhancerActionTypeId:
+ enhancer = info.paramValue(receiverEnhancerActionEnhancerParamTypeId)
+ if enhancer == True:
+ enhancerString = "On"
+ else:
+ enhancerString = "Off"
+ body = bodyStart + '' + enhancerString + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverDialogueLevelActionTypeId:
+ diaLvl = info.paramValue(receiverDialogueLevelActionDialogueLevelParamTypeId)
+ diaStr = str(diaLvl)
+ body = bodyStart + '' + diaStr + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverDialogueLiftActionTypeId:
+ diaLift = info.paramValue(receiverDialogueLiftActionDialogueLiftParamTypeId)
+ diaStr = str(diaLift)
+ body = bodyStart + '' + diaStr + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverBassActionTypeId:
+ bass = info.paramValue(receiverBassActionBassParamTypeId)
+ # bass needs to be multiple of 5
+ remainder = bass % 5
+ bass -= remainder
+ bassStr = str(bass)
+ logger.log("Bass set to", bassStr)
+ body = bodyStart + '' + bassStr + '1dB' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverTrebleActionTypeId:
+ treble = info.paramValue(receiverTrebleActionTrebleParamTypeId)
+ # treble needs to be multiple of 5
+ remainder = treble % 5
+ treble -= remainder
+ trebleStr = str(treble)
+ logger.log("Treble set to", trebleStr)
+ body = bodyStart + '' + trebleStr + '1dB' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverCinemaDSP3DActionTypeId:
+ dsp3D = info.paramValue(receiverCinemaDSP3DActionCinemaDSP3DParamTypeId)
+ logger.log("Cinema DSP 3D set to", dsp3D)
+ body = bodyStart + '<_3D_Cinema_DSP>' + dsp3D + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverAdaptiveDRCActionTypeId:
+ adrc = info.paramValue(receiverAdaptiveDRCActionAdaptiveDRCParamTypeId)
+ logger.log("Adaptive DRC set to", adrc)
+ body = bodyStart + '' + adrc + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverInputSourceActionTypeId or info.actionTypeId == zoneInputSourceActionTypeId:
+ if info.actionTypeId == receiverInputSourceActionTypeId:
+ inputSource = info.paramValue(receiverInputSourceActionInputSourceParamTypeId)
+ else:
+ inputSource = info.paramValue(zoneInputSourceActionInputSourceParamTypeId)
+ logger.log("Input Source changed to", inputSource)
+ body = bodyStart + '' + inputSource + '' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverSurroundModeActionTypeId:
+ surroundMode = info.paramValue(receiverSurroundModeActionSurroundModeParamTypeId)
+ logger.log("Surround Mode changed to", surroundMode)
+ if surroundMode != "Straight":
+ body = bodyStart + '' + surroundMode + '' + bodyEnd
+ else:
+ body = bodyStart + 'On' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverShuffleActionTypeId or info.actionTypeId == zoneShuffleActionTypeId:
+ if info.actionTypeId == receiverShuffleActionTypeId:
+ shuffle = info.paramValue(receiverShuffleActionShuffleParamTypeId)
+ else:
+ shuffle = info.paramValue(zoneShuffleActionShuffleParamTypeId)
+ if shuffle == True:
+ shuffleString = "On"
+ else:
+ shuffleString = "Off"
+ body = '<' + source + '>' + shuffleString + '' + source + '>'
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverRepeatActionTypeId or info.actionTypeId == zoneRepeatActionTypeId:
+ if info.actionTypeId == receiverRepeatActionTypeId:
+ repeat = info.paramValue(receiverRepeatActionRepeatParamTypeId)
+ else:
+ repeat = info.paramValue(zoneRepeatActionRepeatParamTypeId)
+ logger.log("Repeat mode:", repeat)
+ if repeat == "All":
+ repeatString = "All"
+ elif repeat == "One":
+ repeatString = "One"
+ else:
+ repeatString = "Off"
+ body = '<' + source + '>' + repeatString + '' + source + '>'
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ return
+ elif info.actionTypeId == receiverRandomAlbumActionTypeId or info.actionTypeId == zoneRandomAlbumActionTypeId:
+ body = bodyStart + 'SERVER' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ time.sleep(0.5)
+ playRandomAlbum(rUrl, "SERVER")
+ time.sleep(0.5)
+ pollReceiver(info.thing)
+ info.finish(nymea.ThingErrorNoError)
+ else:
+ logger.log("Action not yet implemented for thing")
+ info.finish(nymea.ThingErrorNoError)
+ return
+
+def playRandomAlbum(rUrl, source):
+ # currently source needs to be SERVER
+ # To do: add code to filter out unselectable items
+ if source == "SERVER":
+ browseTree = ["Random", "Music", "By Album", "Random"]
+ logger.log("Playing random album on source " + source)
+ else:
+ browseTree = []
+ logger.log("Source not supported for this action")
+ # navigate browseTree (first item select random server, then folder "Music", ...)
+ menuLayer = browseInTree(rUrl, source, browseTree)
+ # play album by selecting first line --> what if first line is not selectable? filter out non-selectable lines first?
+ if menuLayer == len(browseTree)+1 and menuLayer > 0:
+ # don't do anything unless browsing to the required menu item succeeded
+ selectLine(rUrl, source, 1)
+ return
+
+def browseInTree(rUrl, source, browseTree):
+ menuLayer = 1
+ if browseTree == None:
+ #create empty tree
+ browseTree = []
+ # go up to the main menu level if needed
+ if len(browseTree) > 0:
+ selLayer = 1
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ while menuLayer > selLayer:
+ menuLevelUp(rUrl, source)
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ # navigate browseTree
+ for i in range (0, len(browseTree)):
+ if browseTree[i] == "Random":
+ #browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ currentLine, maxLine = getLineNbrs(browseResponse)
+ selItem = random.randint(1, maxLine)
+ selectLine(rUrl, source, selItem)
+ else:
+ selItem = findLine(rUrl, source, browseTree[i])
+ if selItem > 0:
+ selectLine(rUrl, source, selItem)
+ else:
+ logger.log("Requested item not found")
+ # set menuLayer in case of error in browsing?
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ logger.log("Returning menuLayer", menuLayer)
+ if menuLayer < len(browseTree)+1:
+ logger.log("Attention, this isn't the requested menuLayer!")
+ return menuLayer
+
+def findLine(rUrl, source, searchTxt):
+ # browse menu level: keep going through menu pages (of 8 items per page) until lineTxt is found
+ loop = True
+ selItem = 0
+ gotoLine(rUrl, source, 1)
+ while loop == True:
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ currentLine, maxLine = getLineNbrs(browseResponse)
+ # read the 8 lines in the current browseResponse page
+ for i in range(1, 9):
+ itemTxt, itemAttr = readLine(browseResponse, i)
+ if itemTxt == searchTxt:
+ selItem = currentLine + i - 1
+ loop = False
+ if maxLine > currentLine + 7 and loop == True:
+ # end of list not yet reached, go to next page
+ pageDown(rUrl, source)
+ else:
+ # last page, stop loop
+ loop = False
+ return selItem
+
+def browseThing(browseResult):
+ # To do: add browse menu action "create shortcut here" as soon as nymea allows browse menu actions?
+ # To do: limit browsing to sources that allow it?
+ zoneOrReceiver = browseResult.thing
+ pollReceiver(zoneOrReceiver)
+ if zoneOrReceiver.thingClassId == zoneThingClassId:
+ # get parent receiver thing, needed to get deviceIp
+ for possibleParent in myThings():
+ if possibleParent.id == zoneOrReceiver.parentId:
+ parentReceiver = possibleParent
+ zoneId = zoneOrReceiver.paramValue(zoneThingZoneIdParamTypeId)
+ deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId)
+ bodyStart = ''
+ bodyEnd = ''
+ source = zoneOrReceiver.stateValue(zoneInputSourceStateTypeId)
+ playRandomId = zonePlayRandomBrowserItemActionTypeId
+ powerCheck = zoneOrReceiver.stateValue(zonePowerStateTypeId)
+ elif zoneOrReceiver.thingClassId == receiverThingClassId:
+ parentReceiver = zoneOrReceiver
+ deviceIp = zoneOrReceiver.stateValue(receiverUrlStateTypeId)
+ bodyStart = ''
+ bodyEnd = ''
+ source = zoneOrReceiver.stateValue(receiverInputSourceStateTypeId)
+ playRandomId = receiverPlayRandomBrowserItemActionTypeId
+ powerCheck = zoneOrReceiver.stateValue(receiverPowerStateTypeId)
+ rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ maxItems = 128
+ # maxItems is used to truncate very long lists, as browsing them is very slow due to the nature of Yamaha's API
+ # the value of maxItems needs to be a multiple of 8 to work correctly with the browseResponse pages that contain 8 lines
+ # and it needs to be browsable within nymea's browseThing timeout, which would be around 264-304
+ # but it also seems quite easy to overload the device by making to many API calls, so we limit to 128
+ # (if you want to test this and get stuck, powering off the receiver (not via nymea) should help)
+
+ browsableSources = ["SERVER", "USB"]
+ if source in browsableSources:
+ logger.log("Source %s is browsable" % (source))
+ # turn receiver/zone on if needed, before browsing
+ if powerCheck == False:
+ body = bodyStart + 'On' + bodyEnd
+ rr = requests.post(rUrl, headers=headers, data=body)
+ else:
+ logger.log("Source %s is not browsable" % (source))
+ browseResult.addItem(nymea.BrowserItem("Empty", "Source not browsable", "Non-selectable item", executable=False, disabled=True, icon=nymea.BrowserIconFavorites))
+ browseResult.finish(nymea.ThingErrorNoError)
+ return
+
+ if browseResult.itemId == "":
+ # go to first menu layer
+ selType = "BI"
+ selLayer = 1
+ selItem = 0
+ selTxt = "Main menu"
+ for i in range(1, 6):
+ if i == 1:
+ shortcutId = receiverSettingsBrowsingShortcut1ParamTypeId
+ labelId = receiverSettingsShortcutLabel1ParamTypeId
+ elif i == 2:
+ shortcutId = receiverSettingsBrowsingShortcut2ParamTypeId
+ labelId = receiverSettingsShortcutLabel2ParamTypeId
+ elif i == 3:
+ shortcutId = receiverSettingsBrowsingShortcut3ParamTypeId
+ labelId = receiverSettingsShortcutLabel3ParamTypeId
+ elif i == 4:
+ shortcutId = receiverSettingsBrowsingShortcut4ParamTypeId
+ labelId = receiverSettingsShortcutLabel4ParamTypeId
+ elif i == 5:
+ shortcutId = receiverSettingsBrowsingShortcut5ParamTypeId
+ labelId = receiverSettingsShortcutLabel5ParamTypeId
+ browseTree = parentReceiver.setting(shortcutId)
+ labelTxt = parentReceiver.setting(labelId)
+ if len(browseTree) > 0 and source == "SERVER": # shortcut is configured, and source needs to be server
+ scLayer = len(browseTree) + 1
+ subTxt = "Shortcut to " + browseTree
+ treeInfo = "SC-layer-" + str(scLayer) + "-item-" + str(0) + "-" + browseTree
+ browseResult.addItem(nymea.BrowserItem(treeInfo, labelTxt, subTxt, browsable=True, icon=nymea.BrowserIconFavorites))
+ else:
+ selType, selLayer, selItem, selTxt = splitBrowseItem(browseResult.itemId)
+
+ # go up to the selected menu level if needed
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ while menuLayer > selLayer:
+ menuLevelUp(rUrl, source)
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+
+ if selType == "BI":
+ selectLine(rUrl, source, selItem)
+ elif selType == "EL":
+ # jump to first line of truncated part of list
+ gotoLine(rUrl, source, selItem)
+ elif selType == "SC":
+ # shortcut, browse shortcut tree
+ browseTree = selTxt.split("/")
+ selLayer = browseInTree(rUrl, source, browseTree)
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ if menuLayer == len(browseTree)+1 and menuLayer > 0:
+ # don't do anything unless browsing to the required menu item succeeded
+ logger.log("Browsing to required menu item succeeded")
+ selLayer = len(browseTree)+1
+ selItem = 0
+ selTxt = "Main menu"
+ else:
+ logger.log("Browsing to required menu item unsuccessful")
+ # go up to the selected menu level if needed
+ while menuLayer > selLayer:
+ menuLevelUp(rUrl, source)
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ selLayer = 1
+ selType = "BI"
+ selItem = 0
+ selTxt = "Main menu"
+
+ # browse menu level: keep going through menu pages (of 8 items per page) while last page hasn't been reached
+ loop = True
+ while loop == True:
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ currentLine, maxLine = getLineNbrs(browseResponse)
+ remainder = currentLine % maxItems
+ logger.log("selType", selType, "currentLine", currentLine, "remainder", remainder)
+ # long lists (longer than maxItems) are truncated and can be extended with user action
+ if selType == "BI" and remainder == 1 and currentLine != 1:
+ # truncate list, and create browsable element that will allow user to continue browsing
+ # create info about menu structure (BI = browsable item, EL = extend list in case long list was truncated)
+ treeInfo = "EL-layer-" + str(menuLayer) + "-item-" + str(currentLine) + "-truncated"
+ browseResult.addItem(nymea.BrowserItem(treeInfo, "Continue", "Click to show the next part of this list", browsable=True, icon=nymea.BrowserIconFolder))
+ # truncate results, stop loop
+ loop = False
+ elif selType == "EL" and remainder == 1 and currentLine != selItem:
+ # truncate list again, and create browsable element that will allow user to continue browsing
+ # create info about menu structure (BI = browsable item, EL = extend list in case long list was truncated)
+ treeInfo = "EL-layer-" + str(menuLayer) + "-item-" + str(currentLine) + "-truncated"
+ browseResult.addItem(nymea.BrowserItem(treeInfo, "Continue", "Click to show the next part of this list", browsable=True, icon=nymea.BrowserIconFolder))
+ # truncate results, stop loop
+ loop = False
+ else:
+ # read the 8 lines in the current browseResponse page
+ for i in range(1, 9):
+ itemTxt, itemAttr = readLine(browseResponse, i)
+ itemTxtClean = unescape(itemTxt, {"&": "&", "'": "'", """: '"'})
+ # create info about menu structure (BI = browsable item, EL = extend list in case long list was truncated)
+ treeInfo = "BI-layer-" + str(menuLayer) + "-item-" + str(currentLine+i-1) + "-" + itemTxt
+ if itemAttr == "Container":
+ if source == "SERVER" and menuLayer == 1: # add browserItemAction play random album
+ # change when nymea supports browserItemActions for python plugins:
+ # browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxtClean, browsable=True, icon=nymea.BrowserIconFolder, browserItemActions=playRandomId))
+ browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxtClean, browsable=True, icon=nymea.BrowserIconFolder))
+ else:
+ browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxtClean, browsable=True, icon=nymea.BrowserIconFolder))
+ elif itemAttr == "Item":
+ browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxtClean, executable=True, icon=nymea.BrowserIconMusic))
+ else:
+ # found unselectable item, indicating end of list, stop loop
+ if len(itemTxt) > 0:
+ browseResult.addItem(nymea.BrowserItem(treeInfo, itemTxt, "Not selectable on this receiver", executable=False, disabled=True, icon=nymea.BrowserIconFavorites))
+ else:
+ loop = False
+ if maxLine > currentLine + 7 and loop == True:
+ # end of list not yet reached, go to next page
+ pageDown(rUrl, source)
+ else:
+ # last page, stop loop
+ loop = False
+
+ browseResult.finish(nymea.ThingErrorNoError)
+ return
+
+def executeBrowserItem(info):
+ zoneOrReceiver = info.thing
+ pollReceiver(zoneOrReceiver)
+ if zoneOrReceiver.thingClassId == zoneThingClassId:
+ # get parent receiver thing, needed to get deviceIp
+ for possibleParent in myThings():
+ if possibleParent.id == zoneOrReceiver.parentId:
+ parentReceiver = possibleParent
+ deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId)
+ source = zoneOrReceiver.stateValue(zoneInputSourceStateTypeId)
+ elif zoneOrReceiver.thingClassId == receiverThingClassId:
+ deviceIp = zoneOrReceiver.stateValue(receiverUrlStateTypeId)
+ source = zoneOrReceiver.stateValue(receiverInputSourceStateTypeId)
+ rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
+
+ selType, selLayer, selItem, selTxt = splitBrowseItem(info.itemId)
+
+ # go up to the selected menu level if needed
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ while menuLayer > selLayer:
+ menuLevelUp(rUrl, source)
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+
+ selectLine(rUrl, source, selItem)
+
+ info.finish(nymea.ThingErrorNoError)
+ time.sleep(0.5)
+ pollReceiver(zoneOrReceiver)
+ return
+
+def executeBrowserItemAction(info):
+ if info.actionTypeId == receiverPlayRandomBrowserItemActionTypeId or info.actionTypeId == zonePlayRandomBrowserItemActionTypeId:
+ if info.thing.thingClassId == zoneThingClassId:
+ # get parent receiver thing, needed to get deviceIp
+ for possibleParent in myThings():
+ if possibleParent.id == info.thing.parentId:
+ parentReceiver = possibleParent
+ deviceIp = parentReceiver.stateValue(receiverUrlStateTypeId)
+ zoneId = info.thing.paramValue(zoneThingZoneIdParamTypeId)
+ source = info.thing.stateValue(zoneInputSourceStateTypeId)
+ elif info.thing.thingClassId == receiverThingClassId:
+ deviceIp = info.thing.stateValue(receiverUrlStateTypeId)
+ source = info.thing.stateValue(receiverInputSourceStateTypeId)
+ rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
+ playRandomAlbum(rUrl, source)
+ return
+
+def selectLine(rUrl, source, selItem):
+ if selItem > 0:
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ gotoLine(rUrl, source, 1)
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ currentLine, maxLine = getLineNbrs(browseResponse)
+ while selItem > currentLine + 7:
+ # jump to the list page with the selected line
+ remainder = selItem % 8
+ if remainder == 0:
+ remainder = 8
+ jumpBody = '' + str(selItem - remainder + 1) + ''
+ jr = requests.post(rUrl, headers=headers, data=jumpBody)
+ # confirm we got to right page
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ currentLine, maxLine = getLineNbrs(browseResponse)
+ # now select correct line to go to the next menu level
+ selectBody = '<' + source + '>Line_' + str(selItem - currentLine + 1) + '' + source + '>'
+ sr = requests.post(rUrl, headers=headers, data=selectBody)
+ return
+
+def pageDown(rUrl, source):
+ # scroll to next page of list
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ scrollBody = '<' + source + '>Down' + source + '>'
+ sr = requests.post(rUrl, headers=headers, data=scrollBody)
+ return
+
+def menuLevelUp(rUrl, source):
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ returnBody = '<' + source + '>Return' + source + '>'
+ ur = requests.post(rUrl, headers=headers, data=returnBody)
+ return
+
+def readLine(browseResponse, i):
+ lineResult = []
+ stringIndex1 = browseResponse.find("")
+ stringIndex2 = browseResponse.find("")
+ browseTxt = browseResponse[stringIndex1+8:stringIndex2]
+ stringIndex1 = browseTxt.find("")
+ stringIndex2 = browseTxt.find("")
+ itemTxt = browseTxt[stringIndex1+5:stringIndex2]
+ stringIndex1 = browseTxt.find("")
+ stringIndex2 = browseTxt.find("")
+ itemAttr = browseTxt[stringIndex1+11:stringIndex2]
+ return itemTxt, itemAttr
+
+def splitBrowseItem(itemId):
+ splitId = itemId.split("-",5)
+ selType = splitId[0]
+ selLayer = int(splitId[2])
+ selItem = int(splitId[4])
+ selTxt = splitId[5]
+ return selType, selLayer, selItem, selTxt
+
+def getLineNbrs(browseResponse):
+ stringIndex1 = browseResponse.find("")
+ stringIndex2 = browseResponse.find("")
+ currentLine = int(browseResponse[stringIndex1+14:stringIndex2])
+ stringIndex1 = browseResponse.find("")
+ stringIndex2 = browseResponse.find("")
+ maxLine = int(browseResponse[stringIndex1+10:stringIndex2])
+ return currentLine, maxLine
+
+def gotoLine(rUrl, source, lineNbr):
+ # e.g. line 1: make sure we are on the first line in the menu before continuing
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ browseBody = '<' + source + '>GetParam' + source + '>'
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ jumpBody = '<' + source + '>' + str(lineNbr) + '' + source + '>'
+ jr = requests.post(rUrl, headers=headers, data=jumpBody)
+ return
+
+def browseMenuReady(rUrl, source):
+ # make sure menu status is Ready before sending any further commands, as they may not be processed by the receiver
+ # at same time, return list info as we got it anyway when checking menu status
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ browseBody = '<' + source + '>GetParam' + source + '>'
+ ready = False
+ while ready == False:
+ br = requests.post(rUrl, headers=headers, data=browseBody)
+ browseResponse = br.text
+ stringIndex1 = browseResponse.find("")
+ stringIndex2 = browseResponse.find("")
+ menuStatus = browseResponse[stringIndex1+13:stringIndex2]
+ if menuStatus == "Ready":
+ ready = True
+ stringIndex1 = browseResponse.find("")
+ stringIndex2 = browseResponse.find("")
+ menuLayer = int(browseResponse[stringIndex1+12:stringIndex2])
+ stringIndex1 = browseResponse.find("")
+ stringIndex2 = browseResponse.find("")
+ menuTitle = browseResponse[stringIndex1+11:stringIndex2]
+ logger.log("Menu layer", menuLayer, "Menu title", menuTitle)
+ else:
+ time.sleep(0.1)
+ return browseResponse, menuLayer
+
+def deinit():
+ global pollTimer
+ # If we started a poll timer, cancel it on shutdown.
+ if pollTimer is not None:
+ pollTimer.cancel()
+
+def thingRemoved(thing):
+ logger.log("removeThing called for", thing.name)
+ # Clean up all data related to this thing
+ if pollTimer is not None:
+ pollTimer.cancel()
\ No newline at end of file
diff --git a/yamahaavr/meta.json b/yamahaavr/meta.json
new file mode 100644
index 00000000..cd8b5432
--- /dev/null
+++ b/yamahaavr/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "Yamaha AV receiver",
+ "tagline": "Connect to and control your (non-Musiccast) Yamaha AV receivers",
+ "icon": "yamaha.png",
+ "stability": "consumer",
+ "offline": true,
+ "technologies": [
+ "network"
+ ],
+ "categories": [
+ "multimedia"
+ ]
+}
diff --git a/yamahaavr/requirements.txt b/yamahaavr/requirements.txt
new file mode 100644
index 00000000..1d474356
--- /dev/null
+++ b/yamahaavr/requirements.txt
@@ -0,0 +1,64 @@
+zeroconf==0.29.0 \
+ --hash=sha256:7aefbb658b452b1fd7e51124364f938c6f5e42d6ea893fa2557bea8c06c540af \
+ --hash=sha256:85fdeeef88b08965ab87559177457cfdb5dd2e4bc62a476208c2473a51dfa0b2
+testresources==2.0.1 \
+ --hash=sha256:67a361c3a2412231963b91ab04192209aa91a1aa052f0ab87245dbea889d1282 \
+ --hash=sha256:ee9d1982154a1e212d4e4bac6b610800bfb558e4fb853572a827bc14a96e4417
+requests==2.25.1 \
+ --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 \
+ --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e
+urllib3==1.26.3 \
+ --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \
+ --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73
+chardet==4.0.0 \
+ --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
+ --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5
+lazr.uri==1.0.5 \
+ --hash=sha256:f36e7e40d5f8f2cf20ff2c81784a14a546e6c19c216d40a6617ebe0c96c92c49 \
+ --hash=sha256:71f2faf04b148cf63d78da08ee5d8d6a7a7dbda8c9016b389a16f790d080c06f
+six==1.15.0 \
+ --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \
+ --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced
+wadllib==1.3.5 \
+ --hash=sha256:84fecbaec2fef5ae2d7717a8115d271f18c6b5441eac861c58be8ca57f63c1d3 \
+ --hash=sha256:67d3102b40eefdd6c3007cfbcc4c07f6948fec0666ba5c17d703eab21f054692
+lazr.restfulclient==0.14.3 \
+ --hash=sha256:9f28bbb7c00374159376bd4ce36b4dacde7c6b86a0af625aa5e3ae214651a690 \
+ --hash=sha256:2320e6d132c9a5148895e85be03274bc9305e4605439b03541ee3a618e00fb94
+pyparsing==2.4.7 \
+ --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \
+ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b
+idna==2.5 \
+ --hash=sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab \
+ --hash=sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70
+certifi==2020.12.5 \
+ --hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c \
+ --hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830
+setuptools==54.1.1 \
+ --hash=sha256:1ce82798848a978696465866bb3aaab356003c42d6143e1111fcf069ac838274 \
+ --hash=sha256:75c5c4479f4961f1ffdb597c98aa4e4077e6813685025e8bdebf7598aa84e859
+pbr==5.5.1 \
+ --hash=sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9 \
+ --hash=sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00
+distro==1.5.0 \
+ --hash=sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92 \
+ --hash=sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799
+oauthlib==3.1.0 \
+ --hash=sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889 \
+ --hash=sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea
+requests-oauthlib==1.3.0 \
+ --hash=sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d \
+ --hash=sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a \
+ --hash=sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc
+urllib3==1.26.3 \
+ --hash=sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80 \
+ --hash=sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73
+chardet==4.0.0 \
+ --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
+ --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5
+httplib2==0.19.0 \
+ --hash=sha256:749c32603f9bf16c1277f59531d502e8f1c2ca19901ae653b49c4ed698f0820e \
+ --hash=sha256:e0d428dad43c72dbce7d163b7753ffc7a39c097e6788ef10f4198db69b92f08e
+ifaddr==0.1.7 \
+ --hash=sha256:1f9e8a6ca6f16db5a37d3356f07b6e52344f6f9f7e806d618537731669eb1a94 \
+ --hash=sha256:d1f603952f0a71c9ab4e705754511e4e03b02565bc4cec7188ad6415ff534cd3
\ No newline at end of file
diff --git a/yamahaavr/yamaha.png b/yamahaavr/yamaha.png
new file mode 100644
index 0000000000000000000000000000000000000000..2672fa3b06ca1c5c55dc2f44e32eaffde4b9ed18
GIT binary patch
literal 12812
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
ZqS^0h#He-5{(bL2(@@b?u2zDC{~ym^rY!&f
literal 0
HcmV?d00001
From 54e21a4f218511a25bc2f21ccdffbd7a1c75ad30 Mon Sep 17 00:00:00 2001
From: loosrob <79396812+loosrob@users.noreply.github.com>
Date: Fri, 30 Jul 2021 17:55:08 +0200
Subject: [PATCH 2/8] Improve catching polling errors
try/expect around http requests
---
yamahaavr/integrationpluginyamaha.py | 594 ++++++++++++++-------------
1 file changed, 306 insertions(+), 288 deletions(-)
diff --git a/yamahaavr/integrationpluginyamaha.py b/yamahaavr/integrationpluginyamaha.py
index f3685ead..fa4dd58a 100644
--- a/yamahaavr/integrationpluginyamaha.py
+++ b/yamahaavr/integrationpluginyamaha.py
@@ -48,8 +48,7 @@ class ZeroconfListener(object):
return
pollTimer = None
-
-playPoll = False
+pollFrequency = 40
def discoverThings(info):
if info.thingClassId == receiverThingClassId:
@@ -314,7 +313,7 @@ def setupThing(info):
return
def pollReceiver(info):
- global playPoll
+ global pollFrequency
if info.thingClassId == zoneThingClassId:
# get parent receiver thing, needed to get deviceIp
for possibleParent in myThings():
@@ -333,301 +332,320 @@ def pollReceiver(info):
rUrl = 'http://' + deviceIp + ':80/YamahaRemoteControl/ctrl'
body = bodyStart + 'GetParam' + bodyEnd
headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
- pr = requests.post(rUrl, headers=headers, data=body)
- pollResponse = pr.text
- # add distinction between receiver & zone
- if info.thingClassId == receiverThingClassId:
- receiver = info
- if pr.status_code == requests.codes.ok:
- receiver.setStateValue(receiverConnectedStateTypeId, True)
- # Get power state
- if pollResponse.find("Standby") != -1:
- receiver.setStateValue(receiverPowerStateTypeId, False)
- powerState = False
- elif pollResponse.find("On") != -1:
- receiver.setStateValue(receiverPowerStateTypeId, True)
- powerState = True
- else:
- logger.log("Power state not found!")
- # Get sleep state
- stringIndex1 = pollResponse.find("")
- stringIndex2 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+7:stringIndex2]
- receiver.setStateValue(receiverSleepStateTypeId, responseExtract)
- # Get mute state
- if pollResponse.find("Off") != -1:
- receiver.setStateValue(receiverMuteStateTypeId, False)
- elif pollResponse.find("On") != -1:
- receiver.setStateValue(receiverMuteStateTypeId, True)
- else:
- logger.log("Mute state not found!")
- # Get pure direct state
- if pollResponse.find("Off") != -1:
- receiver.setStateValue(receiverPureDirectStateTypeId, False)
- elif pollResponse.find("On") != -1:
- receiver.setStateValue(receiverPureDirectStateTypeId, True)
- else:
- logger.log("Pure Direct state not found!")
- # Get enhancer state
- if pollResponse.find("Off") != -1:
- receiver.setStateValue(receiverEnhancerStateTypeId, False)
- elif pollResponse.find("On") != -1:
- receiver.setStateValue(receiverEnhancerStateTypeId, True)
- else:
- logger.log("Enhancer state not found!")
- # Get input
- stringIndex1 = pollResponse.find("")
- stringIndex2 = pollResponse.find("")
- inputSource = pollResponse[stringIndex1+18:stringIndex2]
- receiver.setStateValue(receiverInputSourceStateTypeId, inputSource)
- videoSources = ["HDMI1","HDMI2","HDMI3","HDMI4","HDMI5","AV1","AV2","AV3","AV4","AV5","AV6","V-AUX"]
- if inputSource in videoSources:
- receiver.setStateValue(receiverPlayerTypeStateTypeId, "video")
- else:
- receiver.setStateValue(receiverPlayerTypeStateTypeId, "audio")
- # Get sound program
- stringIndex1 = pollResponse.find("")
- stringIndex2 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+15:stringIndex2]
- receiver.setStateValue(receiverSurroundModeStateTypeId, responseExtract)
- # Get Cinema DSP 3D state
- stringIndex1 = pollResponse.find("<_3D_Cinema_DSP>")
- stringIndex2 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+16:stringIndex2]
- receiver.setStateValue(receiverCinemaDSP3DStateTypeId, responseExtract)
- # Get Adaptive DRC state
- stringIndex1 = pollResponse.find("")
- stringIndex2 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+14:stringIndex2]
- receiver.setStateValue(receiverAdaptiveDRCStateTypeId, responseExtract)
- # Get volume - volume is represented by int in Yamaha API, but shown as double = int/10 in Yamaha UI - this is ignored here as nymea wants volume to be an int
- stringIndex1 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+18:stringIndex1+30]
- stringIndex2 = responseExtract.find("")
- responseExtract = responseExtract[0:stringIndex2]
- volume = int(responseExtract)
- receiver.setStateValue(receiverVolumeStateTypeId, volume)
- # Get bass
- stringIndex1 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+11:stringIndex1+30]
- stringIndex2 = responseExtract.find("")
- responseExtract = responseExtract[0:stringIndex2]
- bass = int(responseExtract)
- receiver.setStateValue(receiverBassStateTypeId, bass)
- # Get treble
- stringIndex1 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+13:stringIndex1+30]
- stringIndex2 = responseExtract.find("")
- responseExtract = responseExtract[0:stringIndex2]
- treble = int(responseExtract)
- receiver.setStateValue(receiverTrebleStateTypeId, treble)
- # Get dialogue level
- stringIndex1 = pollResponse.find("")
- stringIndex2 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+14:stringIndex2]
- dialogueLvl = int(responseExtract)
- receiver.setStateValue(receiverDialogueLevelStateTypeId, dialogueLvl)
- # Get dialogue lift
- stringIndex1 = pollResponse.find("")
- stringIndex2 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+15:stringIndex2]
- dialogueLift = int(responseExtract)
- receiver.setStateValue(receiverDialogueLiftStateTypeId, dialogueLift)
- # Get subwoofer trim
- stringIndex1 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+21:stringIndex1+30]
- stringIndex2 = responseExtract.find("")
- responseExtract = responseExtract[0:stringIndex2]
- subTrim = int(responseExtract)
- receiver.setStateValue(receiverSubwooferTrimStateTypeId, subTrim)
- # Get player info
- body = '<' + inputSource + '>GetParam' + inputSource + '>'
- headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
- plr = requests.post(rUrl, headers=headers, data=body)
- if plr.status_code == requests.codes.ok and powerState == True:
- playerResponse = plr.text
- # Get repeat state
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+8:stringIndex2]
- if responseExtract not in ["None", "One", "All"]:
- responseExtract = "None"
- receiver.setStateValue(receiverRepeatStateTypeId, responseExtract)
- # Get shuffle state
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+9:stringIndex2]
- if responseExtract == "On":
- shuffleStatus = True
+ try:
+ pr = requests.post(rUrl, headers=headers, data=body)
+ polled = True
+ except:
+ logger.log("Device didn't respond to http request to get basic status", deviceIp, info.name)
+ polled = False
+ logger.log("Temporarily reducing pollfrequency to give the device some rest")
+ pollFrequency = 120
+ if polled == True:
+ pollResponse = pr.text
+ # add distinction between receiver & zone
+ if info.thingClassId == receiverThingClassId:
+ receiver = info
+ if pr.status_code == requests.codes.ok:
+ receiver.setStateValue(receiverConnectedStateTypeId, True)
+ # Get power state
+ if pollResponse.find("Standby") != -1:
+ receiver.setStateValue(receiverPowerStateTypeId, False)
+ powerState = False
+ elif pollResponse.find("On") != -1:
+ receiver.setStateValue(receiverPowerStateTypeId, True)
+ powerState = True
else:
- shuffleStatus = False
- receiver.setStateValue(receiverShuffleStateTypeId, shuffleStatus)
- # Get playback state
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+15:stringIndex2]
- if responseExtract == "Play":
- playStatus = "Playing"
- playPoll = True or playPoll
- elif responseExtract == "Pause":
- playStatus = "Paused"
- playPoll = True or playPoll
+ logger.log("Power state not found!")
+ # Get sleep state
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+7:stringIndex2]
+ receiver.setStateValue(receiverSleepStateTypeId, responseExtract)
+ # Get mute state
+ if pollResponse.find("Off") != -1:
+ receiver.setStateValue(receiverMuteStateTypeId, False)
+ elif pollResponse.find("On") != -1:
+ receiver.setStateValue(receiverMuteStateTypeId, True)
else:
- playStatus = "Stopped"
- playPoll = False or playPoll
- receiver.setStateValue(receiverPlaybackStatusStateTypeId, playStatus)
- # Get meta info
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+8:stringIndex2]
- receiver.setStateValue(receiverArtistStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+7:stringIndex2]
- receiver.setStateValue(receiverCollectionStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+6:stringIndex2]
- receiver.setStateValue(receiverTitleStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
- # Get artwork --> Yamaha artwork file type isn't recognized by nymea: browse for external cover art?
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+5:stringIndex2]
- artURL = 'http://' + deviceIp + ':80' + responseExtract
- receiver.setStateValue(receiverArtworkStateTypeId, artURL)
- else:
- # Playing from external source so no info available
- receiver.setStateValue(receiverRepeatStateTypeId, "None")
- receiver.setStateValue(receiverShuffleStateTypeId, False)
- receiver.setStateValue(receiverPlaybackStatusStateTypeId, "Stopped")
- receiver.setStateValue(receiverArtistStateTypeId, "")
- receiver.setStateValue(receiverCollectionStateTypeId, "")
- receiver.setStateValue(receiverTitleStateTypeId, "")
- receiver.setStateValue(receiverArtworkStateTypeId, "")
- else:
- receiver.setStateValue(receiverConnectedStateTypeId, False)
- elif info.thingClassId == zoneThingClassId:
- zone = info
- if pr.status_code == requests.codes.ok:
- zone.setStateValue(zoneConnectedStateTypeId, True)
- # Get power state
- if pollResponse.find("Standby") != -1:
- zone.setStateValue(zonePowerStateTypeId, False)
- powerState = False
- elif pollResponse.find("On") != -1:
- zone.setStateValue(zonePowerStateTypeId, True)
- powerState = True
- else:
- logger.log("Power state not found!")
- # Get sleep state
- stringIndex1 = pollResponse.find("")
- stringIndex2 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+7:stringIndex2]
- zone.setStateValue(zoneSleepStateTypeId, responseExtract)
- # Get mute state
- if pollResponse.find("Off") != -1:
- zone.setStateValue(zoneMuteStateTypeId, False)
- elif pollResponse.find("On") != -1:
- zone.setStateValue(zoneMuteStateTypeId, True)
- else:
- logger.log("Mute state not found!")
- # Get input
- stringIndex1 = pollResponse.find("")
- stringIndex2 = pollResponse.find("")
- inputSource = pollResponse[stringIndex1+18:stringIndex2]
- zone.setStateValue(zoneInputSourceStateTypeId, inputSource)
- videoSources = ["HDMI1","HDMI2","HDMI3","HDMI4","HDMI5","AV1","AV2","AV3","AV4","AV5","AV6","V-AUX"]
- if inputSource in videoSources:
- zone.setStateValue(zonePlayerTypeStateTypeId, "video")
- else:
- zone.setStateValue(zonePlayerTypeStateTypeId, "audio")
- # Get volume
- stringIndex1 = pollResponse.find("")
- responseExtract = pollResponse[stringIndex1+18:stringIndex1+30]
- stringIndex2 = responseExtract.find("")
- responseExtract = responseExtract[0:stringIndex2]
- volume = int(responseExtract)
- zone.setStateValue(zoneVolumeStateTypeId, volume)
- # Get player info
- body = '<' + inputSource + '>GetParam' + inputSource + '>'
- headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
- plr = requests.post(rUrl, headers=headers, data=body)
- if plr.status_code == requests.codes.ok and powerState == True:
- playerResponse = plr.text
- # Get repeat state
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+8:stringIndex2]
- if responseExtract not in ["None", "One", "All"]:
- responseExtract = "None"
- zone.setStateValue(zoneRepeatStateTypeId, responseExtract)
- # Get shuffle state
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+9:stringIndex2]
- if responseExtract == "On":
- shuffleStatus = True
+ logger.log("Mute state not found!")
+ # Get pure direct state
+ if pollResponse.find("Off") != -1:
+ receiver.setStateValue(receiverPureDirectStateTypeId, False)
+ elif pollResponse.find("On") != -1:
+ receiver.setStateValue(receiverPureDirectStateTypeId, True)
else:
- shuffleStatus = False
- zone.setStateValue(zoneShuffleStateTypeId, shuffleStatus)
- # Get playback state
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+15:stringIndex2]
- if responseExtract == "Play":
- playStatus = "Playing"
- playPoll = True or playPoll
- elif responseExtract == "Pause":
- playStatus = "Paused"
- playPoll = True or playPoll
+ logger.log("Pure Direct state not found!")
+ # Get enhancer state
+ if pollResponse.find("Off") != -1:
+ receiver.setStateValue(receiverEnhancerStateTypeId, False)
+ elif pollResponse.find("On") != -1:
+ receiver.setStateValue(receiverEnhancerStateTypeId, True)
else:
- playStatus = "Stopped"
- playPoll = False or playPoll
- zone.setStateValue(zonePlaybackStatusStateTypeId, playStatus)
- # Get meta info
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+8:stringIndex2]
- zone.setStateValue(zoneArtistStateTypeId, responseExtract)
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+7:stringIndex2]
- zone.setStateValue(zoneCollectionStateTypeId, responseExtract)
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+6:stringIndex2]
- zone.setStateValue(zoneTitleStateTypeId, responseExtract)
- stringIndex1 = playerResponse.find("")
- stringIndex2 = playerResponse.find("")
- responseExtract = playerResponse[stringIndex1+5:stringIndex2]
- artURL = 'http://' + deviceIp + ':80' + responseExtract
- zone.setStateValue(zoneArtworkStateTypeId, artURL)
+ logger.log("Enhancer state not found!")
+ # Get input
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ inputSource = pollResponse[stringIndex1+18:stringIndex2]
+ receiver.setStateValue(receiverInputSourceStateTypeId, inputSource)
+ videoSources = ["HDMI1","HDMI2","HDMI3","HDMI4","HDMI5","AV1","AV2","AV3","AV4","AV5","AV6","V-AUX"]
+ if inputSource in videoSources:
+ receiver.setStateValue(receiverPlayerTypeStateTypeId, "video")
+ else:
+ receiver.setStateValue(receiverPlayerTypeStateTypeId, "audio")
+ # Get sound program
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+15:stringIndex2]
+ receiver.setStateValue(receiverSurroundModeStateTypeId, responseExtract)
+ # Get Cinema DSP 3D state
+ stringIndex1 = pollResponse.find("<_3D_Cinema_DSP>")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+16:stringIndex2]
+ receiver.setStateValue(receiverCinemaDSP3DStateTypeId, responseExtract)
+ # Get Adaptive DRC state
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+14:stringIndex2]
+ receiver.setStateValue(receiverAdaptiveDRCStateTypeId, responseExtract)
+ # Get volume - volume is represented by int in Yamaha API, but shown as double = int/10 in Yamaha UI - this is ignored here as nymea wants volume to be an int
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+18:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ volume = int(responseExtract)
+ receiver.setStateValue(receiverVolumeStateTypeId, volume)
+ # Get bass
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+11:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ bass = int(responseExtract)
+ receiver.setStateValue(receiverBassStateTypeId, bass)
+ # Get treble
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+13:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ treble = int(responseExtract)
+ receiver.setStateValue(receiverTrebleStateTypeId, treble)
+ # Get dialogue level
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+14:stringIndex2]
+ dialogueLvl = int(responseExtract)
+ receiver.setStateValue(receiverDialogueLevelStateTypeId, dialogueLvl)
+ # Get dialogue lift
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+15:stringIndex2]
+ dialogueLift = int(responseExtract)
+ receiver.setStateValue(receiverDialogueLiftStateTypeId, dialogueLift)
+ # Get subwoofer trim
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+21:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ subTrim = int(responseExtract)
+ receiver.setStateValue(receiverSubwooferTrimStateTypeId, subTrim)
+ # Get player info
+ body = '<' + inputSource + '>GetParam' + inputSource + '>'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ try:
+ plr = requests.post(rUrl, headers=headers, data=body)
+ polled = True
+ except:
+ logger.log("Device didn't respond to http request to get player status", deviceIp, info.name)
+ polled = False
+ logger.log("Temporarily reducing pollfrequency to give the device some rest")
+ pollFrequency = 120
+ if polled == True:
+ if plr.status_code == requests.codes.ok and powerState == True:
+ playerResponse = plr.text
+ # Get repeat state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+8:stringIndex2]
+ if responseExtract not in ["None", "One", "All"]:
+ responseExtract = "None"
+ receiver.setStateValue(receiverRepeatStateTypeId, responseExtract)
+ # Get shuffle state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+9:stringIndex2]
+ if responseExtract == "On":
+ shuffleStatus = True
+ else:
+ shuffleStatus = False
+ receiver.setStateValue(receiverShuffleStateTypeId, shuffleStatus)
+ # Get playback state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+15:stringIndex2]
+ if responseExtract == "Play":
+ playStatus = "Playing"
+ pollFrequency = min(10, pollFrequency)
+ elif responseExtract == "Pause":
+ playStatus = "Paused"
+ pollFrequency = min(10, pollFrequency)
+ else:
+ playStatus = "Stopped"
+ pollFrequency = min(30, pollFrequency)
+ receiver.setStateValue(receiverPlaybackStatusStateTypeId, playStatus)
+ # Get meta info
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+8:stringIndex2]
+ receiver.setStateValue(receiverArtistStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+7:stringIndex2]
+ receiver.setStateValue(receiverCollectionStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+6:stringIndex2]
+ receiver.setStateValue(receiverTitleStateTypeId, unescape(responseExtract, {"&": "&", "'": "'", """: '"'}))
+ # Get artwork --> Yamaha artwork file type isn't recognized by nymea: browse for external cover art?
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+5:stringIndex2]
+ artURL = 'http://' + deviceIp + ':80' + responseExtract
+ receiver.setStateValue(receiverArtworkStateTypeId, artURL)
+ else:
+ # Playing from external source so no info available
+ receiver.setStateValue(receiverRepeatStateTypeId, "None")
+ receiver.setStateValue(receiverShuffleStateTypeId, False)
+ receiver.setStateValue(receiverPlaybackStatusStateTypeId, "Stopped")
+ receiver.setStateValue(receiverArtistStateTypeId, "")
+ receiver.setStateValue(receiverCollectionStateTypeId, "")
+ receiver.setStateValue(receiverTitleStateTypeId, "")
+ receiver.setStateValue(receiverArtworkStateTypeId, "")
else:
- # Playing from external source so no info available
- zone.setStateValue(zoneRepeatStateTypeId, "None")
- zone.setStateValue(zoneShuffleStateTypeId, False)
- zone.setStateValue(zonePlaybackStatusStateTypeId, "Stopped")
- zone.setStateValue(zoneArtistStateTypeId, "")
- zone.setStateValue(zoneCollectionStateTypeId, "")
- zone.setStateValue(zoneTitleStateTypeId, "")
- zone.setStateValue(zoneArtworkStateTypeId, "")
- else:
- zone.setStateValue(zoneConnectedStateTypeId, False)
+ receiver.setStateValue(receiverConnectedStateTypeId, False)
+ elif info.thingClassId == zoneThingClassId:
+ zone = info
+ if pr.status_code == requests.codes.ok:
+ zone.setStateValue(zoneConnectedStateTypeId, True)
+ # Get power state
+ if pollResponse.find("Standby") != -1:
+ zone.setStateValue(zonePowerStateTypeId, False)
+ powerState = False
+ elif pollResponse.find("On") != -1:
+ zone.setStateValue(zonePowerStateTypeId, True)
+ powerState = True
+ else:
+ logger.log("Power state not found!")
+ # Get sleep state
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+7:stringIndex2]
+ zone.setStateValue(zoneSleepStateTypeId, responseExtract)
+ # Get mute state
+ if pollResponse.find("Off") != -1:
+ zone.setStateValue(zoneMuteStateTypeId, False)
+ elif pollResponse.find("On") != -1:
+ zone.setStateValue(zoneMuteStateTypeId, True)
+ else:
+ logger.log("Mute state not found!")
+ # Get input
+ stringIndex1 = pollResponse.find("")
+ stringIndex2 = pollResponse.find("")
+ inputSource = pollResponse[stringIndex1+18:stringIndex2]
+ zone.setStateValue(zoneInputSourceStateTypeId, inputSource)
+ videoSources = ["HDMI1","HDMI2","HDMI3","HDMI4","HDMI5","AV1","AV2","AV3","AV4","AV5","AV6","V-AUX"]
+ if inputSource in videoSources:
+ zone.setStateValue(zonePlayerTypeStateTypeId, "video")
+ else:
+ zone.setStateValue(zonePlayerTypeStateTypeId, "audio")
+ # Get volume
+ stringIndex1 = pollResponse.find("")
+ responseExtract = pollResponse[stringIndex1+18:stringIndex1+30]
+ stringIndex2 = responseExtract.find("")
+ responseExtract = responseExtract[0:stringIndex2]
+ volume = int(responseExtract)
+ zone.setStateValue(zoneVolumeStateTypeId, volume)
+ # Get player info
+ body = '<' + inputSource + '>GetParam' + inputSource + '>'
+ headers = {'Content-Type': 'text/xml', 'Accept': '*/*'}
+ try:
+ plr = requests.post(rUrl, headers=headers, data=body)
+ polled = True
+ except:
+ logger.log("Device didn't respond to http request to get player status", deviceIp, info.name)
+ polled = False
+ logger.log("Temporarily reducing pollfrequency to give the device some rest")
+ pollFrequency = 120
+ if polled == True:
+ if plr.status_code == requests.codes.ok and powerState == True:
+ playerResponse = plr.text
+ # Get repeat state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+8:stringIndex2]
+ if responseExtract not in ["None", "One", "All"]:
+ responseExtract = "None"
+ zone.setStateValue(zoneRepeatStateTypeId, responseExtract)
+ # Get shuffle state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+9:stringIndex2]
+ if responseExtract == "On":
+ shuffleStatus = True
+ else:
+ shuffleStatus = False
+ zone.setStateValue(zoneShuffleStateTypeId, shuffleStatus)
+ # Get playback state
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+15:stringIndex2]
+ if responseExtract == "Play":
+ playStatus = "Playing"
+ pollFrequency = min(10, pollFrequency)
+ elif responseExtract == "Pause":
+ playStatus = "Paused"
+ pollFrequency = min(10, pollFrequency)
+ else:
+ playStatus = "Stopped"
+ pollFrequency = min(30, pollFrequency)
+ zone.setStateValue(zonePlaybackStatusStateTypeId, playStatus)
+ # Get meta info
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+8:stringIndex2]
+ zone.setStateValue(zoneArtistStateTypeId, responseExtract)
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+7:stringIndex2]
+ zone.setStateValue(zoneCollectionStateTypeId, responseExtract)
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+6:stringIndex2]
+ zone.setStateValue(zoneTitleStateTypeId, responseExtract)
+ stringIndex1 = playerResponse.find("")
+ stringIndex2 = playerResponse.find("")
+ responseExtract = playerResponse[stringIndex1+5:stringIndex2]
+ artURL = 'http://' + deviceIp + ':80' + responseExtract
+ zone.setStateValue(zoneArtworkStateTypeId, artURL)
+ else:
+ # Playing from external source so no info available
+ zone.setStateValue(zoneRepeatStateTypeId, "None")
+ zone.setStateValue(zoneShuffleStateTypeId, False)
+ zone.setStateValue(zonePlaybackStatusStateTypeId, "Stopped")
+ zone.setStateValue(zoneArtistStateTypeId, "")
+ zone.setStateValue(zoneCollectionStateTypeId, "")
+ zone.setStateValue(zoneTitleStateTypeId, "")
+ zone.setStateValue(zoneArtworkStateTypeId, "")
+ else:
+ zone.setStateValue(zoneConnectedStateTypeId, False)
def pollService():
logger.log("pollService!!!")
global pollTimer
- global playPoll
- # restart the timer for next poll (if player was playing at previous poll, increase poll frequency)
- # we start the timer before polling the receivers to test if this avoids the timer not restarting due to request errors in pollReceiver
- if playPoll == True:
- interval = 10
- else:
- interval = 30
- logger.log("Restarting timer @ pollService")
- pollTimer = threading.Timer(interval, pollService)
+ global pollFrequency
+ # restart the timer for next poll
+ logger.log("Restarting timer @ pollService, with frequency", pollFrequency)
+ pollTimer = threading.Timer(pollFrequency, pollService)
pollTimer.start()
- playPoll = False
+ pollFrequency = 30
# Poll all receivers we know
for thing in myThings():
if thing.thingClassId == receiverThingClassId or thing.thingClassId == zoneThingClassId:
From 0d961ce9b00d132ad788b06edb2fefaad4ef17d1 Mon Sep 17 00:00:00 2001
From: loosrob <79396812+loosrob@users.noreply.github.com>
Date: Tue, 17 Aug 2021 16:09:54 +0200
Subject: [PATCH 3/8] update debian/control
---
debian/control | 17 +++++++++++++++++
debian/nymea-plugin-yamaha.install.in | 3 +++
2 files changed, 20 insertions(+)
create mode 100644 debian/nymea-plugin-yamaha.install.in
diff --git a/debian/control b/debian/control
index ccb78d60..945e1a6f 100644
--- a/debian/control
+++ b/debian/control
@@ -1080,6 +1080,22 @@ Description: nymea.io plugin to monitor the ISS position
This package will install the nymea.io plugin for wheretheiss.at
+Package: nymea-plugin-yamaha
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends},
+ nymea-plugins-translations,
+ python3-pip,
+Description: nymea.io plugin for Yamaha receivers
+ The nymea daemon is a plugin based IoT (Internet of Things) server. The
+ server works like a translator for devices, things and services and
+ allows them to interact.
+ With the powerful rule engine you are able to connect any device available
+ in the system and create individual scenes and behaviors for your environment.
+ .
+ This package will install the nymea.io plugin for (non-Musiccast) Yamaha AV receivers.
+
+
Package: nymea-plugin-zigbee-develco
Architecture: any
Depends: ${shlibs:Depends},
@@ -1237,6 +1253,7 @@ Depends: nymea-plugin-anel,
nymea-plugin-keba,
nymea-plugin-unifi,
nymea-plugin-usbrelay,
+ Nymea-plugin-yamaha,
nymea-plugins-zigbee,
Description: Plugins for nymea IoT server - the default plugin collection
The nymea daemon is a plugin based IoT (Internet of Things) server. The
diff --git a/debian/nymea-plugin-yamaha.install.in b/debian/nymea-plugin-yamaha.install.in
new file mode 100644
index 00000000..c04ed93c
--- /dev/null
+++ b/debian/nymea-plugin-yamaha.install.in
@@ -0,0 +1,3 @@
+yamahaavr/integrationpluginyamaha.json usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
+yamahaavr/integrationpluginyamaha.py usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
+yamahaavr/requirements.txt usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
From 0a89c988d6ec77a591e43759c27742604158d875 Mon Sep 17 00:00:00 2001
From: loosrob <79396812+loosrob@users.noreply.github.com>
Date: Tue, 17 Aug 2021 16:44:09 +0200
Subject: [PATCH 4/8] correct typo in control
---
debian/control | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/debian/control b/debian/control
index 945e1a6f..11a09d71 100644
--- a/debian/control
+++ b/debian/control
@@ -1253,7 +1253,7 @@ Depends: nymea-plugin-anel,
nymea-plugin-keba,
nymea-plugin-unifi,
nymea-plugin-usbrelay,
- Nymea-plugin-yamaha,
+ nymea-plugin-yamaha,
nymea-plugins-zigbee,
Description: Plugins for nymea IoT server - the default plugin collection
The nymea daemon is a plugin based IoT (Internet of Things) server. The
From 69ae93ed61690e4536aa639012656a6f5b3a8d5e Mon Sep 17 00:00:00 2001
From: loosrob <79396812+loosrob@users.noreply.github.com>
Date: Tue, 17 Aug 2021 18:15:59 +0200
Subject: [PATCH 5/8] fix folder vs plugin name
---
debian/control | 4 ++--
debian/nymea-plugin-yamaha.install.in | 3 ---
debian/nymea-plugin-yamahaavr.install.in | 3 +++
...ationpluginyamaha.json => integrationpluginyamahaavr.json} | 0
...tegrationpluginyamaha.py => integrationpluginyamahaavr.py} | 0
5 files changed, 5 insertions(+), 5 deletions(-)
delete mode 100644 debian/nymea-plugin-yamaha.install.in
create mode 100644 debian/nymea-plugin-yamahaavr.install.in
rename yamahaavr/{integrationpluginyamaha.json => integrationpluginyamahaavr.json} (100%)
rename yamahaavr/{integrationpluginyamaha.py => integrationpluginyamahaavr.py} (100%)
diff --git a/debian/control b/debian/control
index 11a09d71..afa108d7 100644
--- a/debian/control
+++ b/debian/control
@@ -1080,7 +1080,7 @@ Description: nymea.io plugin to monitor the ISS position
This package will install the nymea.io plugin for wheretheiss.at
-Package: nymea-plugin-yamaha
+Package: nymea-plugin-yamahaavr
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
@@ -1253,7 +1253,7 @@ Depends: nymea-plugin-anel,
nymea-plugin-keba,
nymea-plugin-unifi,
nymea-plugin-usbrelay,
- nymea-plugin-yamaha,
+ nymea-plugin-yamahaavr,
nymea-plugins-zigbee,
Description: Plugins for nymea IoT server - the default plugin collection
The nymea daemon is a plugin based IoT (Internet of Things) server. The
diff --git a/debian/nymea-plugin-yamaha.install.in b/debian/nymea-plugin-yamaha.install.in
deleted file mode 100644
index c04ed93c..00000000
--- a/debian/nymea-plugin-yamaha.install.in
+++ /dev/null
@@ -1,3 +0,0 @@
-yamahaavr/integrationpluginyamaha.json usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
-yamahaavr/integrationpluginyamaha.py usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
-yamahaavr/requirements.txt usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
diff --git a/debian/nymea-plugin-yamahaavr.install.in b/debian/nymea-plugin-yamahaavr.install.in
new file mode 100644
index 00000000..187b1855
--- /dev/null
+++ b/debian/nymea-plugin-yamahaavr.install.in
@@ -0,0 +1,3 @@
+yamahaavr/integrationpluginyamahaavr.json usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
+yamahaavr/integrationpluginyamahaavr.py usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
+yamahaavr/requirements.txt usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
\ No newline at end of file
diff --git a/yamahaavr/integrationpluginyamaha.json b/yamahaavr/integrationpluginyamahaavr.json
similarity index 100%
rename from yamahaavr/integrationpluginyamaha.json
rename to yamahaavr/integrationpluginyamahaavr.json
diff --git a/yamahaavr/integrationpluginyamaha.py b/yamahaavr/integrationpluginyamahaavr.py
similarity index 100%
rename from yamahaavr/integrationpluginyamaha.py
rename to yamahaavr/integrationpluginyamahaavr.py
From 4cd11e2378e65869336799913dd04854cf9eca61 Mon Sep 17 00:00:00 2001
From: loosrob <79396812+loosrob@users.noreply.github.com>
Date: Sat, 21 Aug 2021 12:58:32 +0200
Subject: [PATCH 6/8] Update README.md
---
yamahaavr/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/yamahaavr/README.md b/yamahaavr/README.md
index 24174267..4d466fb3 100644
--- a/yamahaavr/README.md
+++ b/yamahaavr/README.md
@@ -21,7 +21,7 @@ As a nice extra, a random album on a random server can be started with a simple
## Requirements
* nymea and the Yamaha device must be in the same local area network.
-* The package "nymea-plugin-yamaha" must be installed.
+* The package "nymea-plugin-yamahaavr" must be installed.
## More
From 6acc0370306a6c2464074f1b9ab2000c2c3e9986 Mon Sep 17 00:00:00 2001
From: loosrob <79396812+loosrob@users.noreply.github.com>
Date: Thu, 26 Aug 2021 14:44:38 +0200
Subject: [PATCH 7/8] Use plugintimer, fix to random album, zeroconf to module
---
yamahaavr/integrationpluginyamahaavr.py | 117 ++++++------------------
yamahaavr/zeroconfbrowser.py | 74 +++++++++++++++
2 files changed, 102 insertions(+), 89 deletions(-)
create mode 100644 yamahaavr/zeroconfbrowser.py
diff --git a/yamahaavr/integrationpluginyamahaavr.py b/yamahaavr/integrationpluginyamahaavr.py
index fa4dd58a..6e5a074a 100644
--- a/yamahaavr/integrationpluginyamahaavr.py
+++ b/yamahaavr/integrationpluginyamahaavr.py
@@ -5,50 +5,10 @@ import json
import requests
import random
from xml.sax.saxutils import unescape
-from zeroconf import IPVersion, ServiceBrowser, ServiceInfo, Zeroconf
-from typing import Callable, List
-
-class ZeroconfDevice(object):
- # To do: replace with nymea serviceBrowser
- def __init__(self, name: str, ip: str, port: int, model: str, id: str) -> None:
- self.name = name
- self.ip = ip
- self.port = port
- self.model = model
- self.id = id
-
- def __repr__(self) -> str:
- return f"{type(self).__name__}({self.__dict__})"
-
- def __eq__(self, other) -> bool:
- return self is other or self.__dict__ == other.__dict__
-
-class ZeroconfListener(object):
- # To do: replace with nymea serviceBrowser
- """Basic zeroconf listener."""
-
- def __init__(self, func: Callable[[ServiceInfo], None]) -> None:
- """Initialize zeroconf listener with function callback."""
- self._func = func
-
- def __repr__(self) -> str:
- return f"{type(self).__name__}({self.__dict__})"
-
- def __eq__(self, other) -> bool:
- return self is other or self.__dict__ == other.__dict__
-
- def add_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
- """Callback function when zeroconf service is discovered."""
- self._func(zeroconf.get_service_info(type, name))
-
- def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
- return
-
- def remove_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
- return
+from zeroconfbrowser import ZeroconfDevice, ZeroconfListener, discover
pollTimer = None
-pollFrequency = 40
+pollFrequency = 30
def discoverThings(info):
if info.thingClassId == receiverThingClassId:
@@ -202,38 +162,6 @@ def findIps():
discoveredIps.append(deviceInfo.ip)
return discoveredIps
-def discover(service_type: str, timeout: int = 5) -> List[ZeroconfDevice]:
- # To do: replace with nymea serviceBrowser
- """From pyvizio: Return all discovered zeroconf services of a given service type over given timeout period."""
- services = []
-
- def append_service(info: ServiceInfo) -> None:
- """Append discovered zeroconf service to service list."""
- name = info.name[: -(len(info.type) + 1)]
- ip = info.parsed_addresses(IPVersion.V4Only)[0]
- port = info.port
- model = info.properties.get(b"name", "")
- id = info.properties.get(b"id")
-
- # handle id decode for various discovered use cases
- if isinstance(id, bytes):
- try:
- int(id, 16)
- except Exception:
- id = id.hex()
- else:
- id = None
-
- service = ZeroconfDevice(name, ip, port, model, id)
- services.append(service)
-
- zeroconf = Zeroconf()
- ServiceBrowser(zeroconf, service_type, ZeroconfListener(append_service))
- time.sleep(timeout)
- zeroconf.close()
-
- return services
-
def setupThing(info):
if info.thing.thingClassId == receiverThingClassId:
searchSystemId = info.thing.paramValue(receiverThingSerialParamTypeId)
@@ -278,10 +206,11 @@ def setupThing(info):
# If no poll timer is set up yet, start it now
logger.log("Creating pollService")
global pollTimer
+ global pollFrequency
if pollTimer == None:
logger.log("Starting timer @ setupThing")
- pollTimer = threading.Timer(30, pollService)
- pollTimer.start()
+ pollTimer = nymea.PluginTimer(pollFrequency, pollService)
+ logger.log("timer interval @ setupThing", pollTimer.interval)
else:
logger.log("Timer already exists @ setupThing")
@@ -638,13 +567,12 @@ def pollReceiver(info):
zone.setStateValue(zoneConnectedStateTypeId, False)
def pollService():
- logger.log("pollService!!!")
+ logger.log("pollTimer triggered")
global pollTimer
global pollFrequency
- # restart the timer for next poll
- logger.log("Restarting timer @ pollService, with frequency", pollFrequency)
- pollTimer = threading.Timer(pollFrequency, pollService)
- pollTimer.start()
+ logger.log("adjusting timer interval")
+ pollTimer.interval = pollFrequency
+ logger.log("timer interval @ pollService", pollTimer.interval)
pollFrequency = 30
# Poll all receivers we know
for thing in myThings():
@@ -1016,10 +944,21 @@ def playRandomAlbum(rUrl, source):
logger.log("Source not supported for this action")
# navigate browseTree (first item select random server, then folder "Music", ...)
menuLayer = browseInTree(rUrl, source, browseTree)
- # play album by selecting first line --> what if first line is not selectable? filter out non-selectable lines first?
- if menuLayer == len(browseTree)+1 and menuLayer > 0:
- # don't do anything unless browsing to the required menu item succeeded
- selectLine(rUrl, source, 1)
+
+ # don't do anything unless browsing to the required menu item succeeded:
+ if menuLayer == len(browseTree)+1 and menuLayer > 0:
+ # play album by selecting first "selectable" line (attribute = "Item")
+ browseResponse, menuLayer = browseMenuReady(rUrl, source)
+ selectable = False
+ line = 1
+ while selectable == False:
+ itemTxt, itemAttr = readLine(browseResponse, line)
+ if itemAttr == "Item":
+ selectable = True
+ else:
+ line += 1
+ logger.log("Selecting line %s with label %s" % (line, itemTxt))
+ selectLine(rUrl, source, line)
return
def browseInTree(rUrl, source, browseTree):
@@ -1037,7 +976,6 @@ def browseInTree(rUrl, source, browseTree):
# navigate browseTree
for i in range (0, len(browseTree)):
if browseTree[i] == "Random":
- #browseResponse, menuLayer = browseMenuReady(rUrl, source)
currentLine, maxLine = getLineNbrs(browseResponse)
selItem = random.randint(1, maxLine)
selectLine(rUrl, source, selItem)
@@ -1391,10 +1329,11 @@ def deinit():
global pollTimer
# If we started a poll timer, cancel it on shutdown.
if pollTimer is not None:
- pollTimer.cancel()
+ pollTimer = None
def thingRemoved(thing):
+ global pollTimer
logger.log("removeThing called for", thing.name)
# Clean up all data related to this thing
- if pollTimer is not None:
- pollTimer.cancel()
\ No newline at end of file
+ if len(myThings()) == 0 and pollTimer is not None:
+ pollTimer = None
\ No newline at end of file
diff --git a/yamahaavr/zeroconfbrowser.py b/yamahaavr/zeroconfbrowser.py
new file mode 100644
index 00000000..ea047afb
--- /dev/null
+++ b/yamahaavr/zeroconfbrowser.py
@@ -0,0 +1,74 @@
+import time
+from zeroconf import IPVersion, ServiceBrowser, ServiceInfo, Zeroconf
+from typing import Callable, List
+
+class ZeroconfDevice(object):
+ # To do: replace with nymea serviceBrowser
+ def __init__(self, name: str, ip: str, port: int, model: str, id: str) -> None:
+ self.name = name
+ self.ip = ip
+ self.port = port
+ self.model = model
+ self.id = id
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({self.__dict__})"
+
+ def __eq__(self, other) -> bool:
+ return self is other or self.__dict__ == other.__dict__
+
+class ZeroconfListener(object):
+ # To do: replace with nymea serviceBrowser
+ """Basic zeroconf listener."""
+
+ def __init__(self, func: Callable[[ServiceInfo], None]) -> None:
+ """Initialize zeroconf listener with function callback."""
+ self._func = func
+
+ def __repr__(self) -> str:
+ return f"{type(self).__name__}({self.__dict__})"
+
+ def __eq__(self, other) -> bool:
+ return self is other or self.__dict__ == other.__dict__
+
+ def add_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
+ """Callback function when zeroconf service is discovered."""
+ self._func(zeroconf.get_service_info(type, name))
+
+ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
+ return
+
+ def remove_service(self, zeroconf: Zeroconf, type: str, name: str) -> None:
+ return
+
+def discover(service_type: str, timeout: int = 5) -> List[ZeroconfDevice]:
+ # To do: replace with nymea serviceBrowser
+ """From pyvizio: Return all discovered zeroconf services of a given service type over given timeout period."""
+ services = []
+
+ def append_service(info: ServiceInfo) -> None:
+ """Append discovered zeroconf service to service list."""
+ name = info.name[: -(len(info.type) + 1)]
+ ip = info.parsed_addresses(IPVersion.V4Only)[0]
+ port = info.port
+ model = info.properties.get(b"name", "")
+ id = info.properties.get(b"id")
+
+ # handle id decode for various discovered use cases
+ if isinstance(id, bytes):
+ try:
+ int(id, 16)
+ except Exception:
+ id = id.hex()
+ else:
+ id = None
+
+ service = ZeroconfDevice(name, ip, port, model, id)
+ services.append(service)
+
+ zeroconf = Zeroconf()
+ ServiceBrowser(zeroconf, service_type, ZeroconfListener(append_service))
+ time.sleep(timeout)
+ zeroconf.close()
+
+ return services
\ No newline at end of file
From 3b68a7a8daaeb9cde2be5e7afaa1f39f84d72c91 Mon Sep 17 00:00:00 2001
From: loosrob <79396812+loosrob@users.noreply.github.com>
Date: Thu, 2 Sep 2021 14:03:15 +0200
Subject: [PATCH 8/8] add zeroconf browser to install.in
---
debian/nymea-plugin-yamahaavr.install.in | 1 +
1 file changed, 1 insertion(+)
diff --git a/debian/nymea-plugin-yamahaavr.install.in b/debian/nymea-plugin-yamahaavr.install.in
index 187b1855..b95cf3f3 100644
--- a/debian/nymea-plugin-yamahaavr.install.in
+++ b/debian/nymea-plugin-yamahaavr.install.in
@@ -1,3 +1,4 @@
yamahaavr/integrationpluginyamahaavr.json usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
yamahaavr/integrationpluginyamahaavr.py usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
+yamahaavr/zeroconfbrowser.py usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
yamahaavr/requirements.txt usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/yamahaavr/
\ No newline at end of file