diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml
new file mode 100644
index 0000000..f540664
--- /dev/null
+++ b/.github/workflows/tox.yml
@@ -0,0 +1,29 @@
+name: Tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: [3.7, 3.8, 3.9]
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install tox tox-gh-actions
+ - name: Test with tox
+ run: tox
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b481729..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-language: python
-matrix:
- fast_finish: true
- include:
- - python: "2.7"
- env: TOXENV=py27
- - python: "3.5"
- env: TOXENV=py35
- - python: "3.6"
- env: TOXENV=py36
-install: pip install tox
-script: tox
diff --git a/MANIFEST.in b/MANIFEST.in
index e69de29..e1e0072 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+ include *.rst
+ include Makefile
+ recursive-include tests *.py
+ recursive-include tests *.xml
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 518b22c..0000000
--- a/Makefile
+++ /dev/null
@@ -1,12 +0,0 @@
-test:
- flake8
- isort --recursive --check-only --diff
- tox
-
-isort:
- isort --recursive --apply
-
-clean:
- rm -rf .virtualenv dist build *.egg-info
-
-.PHONY: clean isort test
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 8c3422d..0a5fed5 100644
--- a/README.rst
+++ b/README.rst
@@ -1,13 +1,10 @@
rxv
===
-.. image:: https://travis-ci.org/wuub/rxv.svg?branch=master
- :target: https://travis-ci.org/wuub/rxv
+.. image:: https://badge.fury.io/py/rxv.svg
+ :target: https://badge.fury.io/py/rxv
-.. image:: https://landscape.io/github/wuub/rxv/master/landscape.svg?style=flat
- :target: https://landscape.io/github/wuub/rxv/master
- :alt: Code Health
-Automation Library for Yamaha RX-V473, RX-V573, RX-V673, RX-V773 receivers
+Automation Library for Yamaha receivers
Installation
============
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2e8be56
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,6 @@
+[build-system]
+requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+write_to = "rxv/version.py"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 945c9b4..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-.
\ No newline at end of file
diff --git a/rxv/.gitignore b/rxv/.gitignore
new file mode 100644
index 0000000..9852786
--- /dev/null
+++ b/rxv/.gitignore
@@ -0,0 +1 @@
+version.py
diff --git a/rxv/__init__.py b/rxv/__init__.py
index e71b14e..a6f11e3 100644
--- a/rxv/__init__.py
+++ b/rxv/__init__.py
@@ -16,12 +16,4 @@
def find(timeout=1.5):
"""Find all Yamah receivers on local network using SSDP search."""
- return [
- RXV(
- ctrl_url=ri.ctrl_url,
- model_name=ri.model_name,
- friendly_name=ri.friendly_name,
- unit_desc_url=ri.unit_desc_url
- )
- for ri in ssdp.discover(timeout=timeout)
- ]
+ return [RXV(**ri._asdict()) for ri in ssdp.discover(timeout=timeout)]
diff --git a/rxv/rxv.py b/rxv/rxv.py
index 7862d5b..97f5b19 100644
--- a/rxv/rxv.py
+++ b/rxv/rxv.py
@@ -50,6 +50,7 @@ def __init__(self, play=False, stop=False, pause=False,
YamahaCommand = '{payload}'
Zone = '<{zone}>{request_text}{zone}>'
BasicStatusGet = 'GetParam'
+PartyMode = '{state}'
PowerControl = '{state}'
PowerControlSleep = '{sleep_value}'
Input = '{input_name}'
@@ -67,6 +68,8 @@ def __init__(self, play=False, stop=False, pause=False,
VolumeMute = '{state}'
SelectNetRadioLine = 'Line_{lineno}'\
''
+SelectServerLine = 'Line_{lineno}'\
+ ''
HdmiOut = ''
@@ -84,8 +87,8 @@ def __init__(self, play=False, stop=False, pause=False,
class RXV(object):
def __init__(self, ctrl_url, model_name="Unknown",
- zone="Main_Zone", friendly_name='Unknown',
- unit_desc_url=None):
+ serial_number=None, zone="Main_Zone",
+ friendly_name='Unknown', unit_desc_url=None):
if re.match(r"\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}", ctrl_url):
# backward compatibility: accept ip address as a contorl url
warnings.warn("Using IP address as a Control URL is deprecated")
@@ -93,6 +96,7 @@ def __init__(self, ctrl_url, model_name="Unknown",
self.ctrl_url = ctrl_url
self.unit_desc_url = unit_desc_url or re.sub('ctrl$', 'desc.xml', ctrl_url)
self.model_name = model_name
+ self.serial_number = serial_number
self.friendly_name = friendly_name
self._inputs_cache = None
self._zones_cache = None
@@ -330,7 +334,7 @@ def surround_programs(self):
if source_xml is None:
return False
- setup = source_xml.find('.//*[@Title_1="Setup"]')
+ setup = source_xml.find('.//Menu[@Title_1="Setup"]')
if setup is None:
return False
@@ -590,6 +594,22 @@ def volume_fade(self, final_vol, sleep=0.5):
self.volume = val
time.sleep(sleep)
+ @property
+ def partymode(self):
+ request_text = PartyMode.format(state=GetParam)
+ response = self._request('GET', request_text, False)
+ pmode = response.find('System/Party_Mode/Mode').text
+ assert pmode in ["On", "Off"]
+ return pmode == "On"
+
+ @partymode.setter
+ def partymode(self, state):
+ assert state in [True, False]
+ new_state = "On" if state else "Off"
+ request_text = PartyMode.format(state=new_state)
+ response = self._request('PUT', request_text, False)
+ return response
+
@property
def mute(self):
request_text = VolumeMute.format(state=GetParam)
@@ -642,6 +662,39 @@ def net_radio(self, path):
# print("Sleeping because we are not ready yet")
time.sleep(1)
+ def _direct_sel_server(self, lineno):
+ request_text = SelectServerLine.format(lineno=lineno)
+ return self._request('PUT', request_text, zone_cmd=False)
+
+ def server(self, path):
+ """Play from specified server
+
+ This lets you play a SERVER address in a single command
+ with by encoding it with > as separators. For instance:
+
+ Server>Playlists>GoodVibes
+
+ This code is copied from the net_radio function.
+
+ TODO: better error handling if we some how time out
+ """
+ layers = path.split(">")
+ self.input = "SERVER"
+
+ for attempt in range(20):
+ menu = self.menu_status()
+ if menu.ready:
+ for line, value in menu.current_list.items():
+ if value == layers[menu.layer - 1]:
+ lineno = line[5:]
+ self._direct_sel_server(lineno)
+ if menu.layer == len(layers):
+ return
+ break
+ else:
+ # print("Sleeping because we are not ready yet")
+ time.sleep(1)
+
@property
def sleep(self):
request_text = PowerControlSleep.format(sleep_value=GetParam)
diff --git a/rxv/ssdp.py b/rxv/ssdp.py
index 5d62d56..180c0af 100644
--- a/rxv/ssdp.py
+++ b/rxv/ssdp.py
@@ -37,8 +37,15 @@
"{urn:schemas-upnp-org:device-1-0}device"
"/{urn:schemas-upnp-org:device-1-0}friendlyName"
)
+SERIAL_NUMBER_QUERY = (
+ "{urn:schemas-upnp-org:device-1-0}device"
+ "/{urn:schemas-upnp-org:device-1-0}serialNumber"
+)
-RxvDetails = namedtuple("RxvDetails", "ctrl_url unit_desc_url, model_name friendly_name")
+RxvDetails = namedtuple(
+ "RxvDetails",
+ "ctrl_url unit_desc_url, model_name friendly_name serial_number"
+)
def discover(timeout=1.5):
@@ -85,8 +92,9 @@ def rxv_details(location):
unit_desc_url = urljoin(url_base_el.text, unit_desc_url_local)
model_name = res.find(MODEL_NAME_QUERY).text
friendly_name = res.find(FRIENDLY_NAME_QUERY).text
+ serial_number = res.find(SERIAL_NUMBER_QUERY).text
- return RxvDetails(ctrl_url, unit_desc_url, model_name, friendly_name)
+ return RxvDetails(ctrl_url, unit_desc_url, model_name, friendly_name, serial_number)
if __name__ == '__main__':
diff --git a/setup.cfg b/setup.cfg
index ba1e011..64e52ed 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,94 @@
+[metadata]
+name = rxv
+description = Automation Library for Yamaha RX-V473, RX-V573, RX-V673, RX-V773 receivers
+long_description = file: README.md
+long_description_content_type = text/x-rst
+maintainer = Wojciech Bederski
+maintainer-email = github@wuub.net
+author = Wojciech Bederski
+author-email = github@wuub.net
+url = https://github.com/wuub/rxv
+project_urls =
+ Source=https://github.com/wuub/rxv
+ Tracker=https://github.com/wuub/rxv/issues
+platforms = any
+license = BSD
+license_file = LICENSE
+classifiers =
+ Development Status :: 5 - Production/Stable
+ Framework :: tox
+ Intended Audience :: Developers
+ License :: OSI Approved :: BSD License
+ Operating System :: OS Independent
+ Topic :: Home Automation,
+ Topic :: Software Development :: Libraries
+ Topic :: Utilities
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: Implementation :: CPython
+
+
+[options]
+zip_safe = False
+include_package_data = True
+packages = find:
+install_requires =
+ requests
+ defusedxml
+
+[options.extras_require]
+testing =
+ black
+ flake8
+ mock
+ pytest
+ pytest-cov
+ pytest-timeout
+ requests-mock
+
[wheel]
universal = 1
+[tox:tox]
+minversion = 3.7
+isolated_build = true
+skip_missing_interpreters = true
+envlist =
+ flake8
+ manifest
+ py{37,38,39}
+
+[gh-actions]
+python =
+ 3.7: py37
+ 3.8: py38, flake8, manifest
+ 3.9: py39
+
+[coverage:run]
+source_pkgs=
+ rxv
+
+[testenv]
+extras = testing
+commands = py.test --cov --cov-report=term --cov-report=xml {posargs}
+
+[testenv:flake8]
+description = run flake8 under {basepython}
+commands = flake8 rxv/ tests/
+extras = testing
+
+[testenv:manifest]
+basepython = python3.8
+deps = check-manifest
+skip_install = true
+commands = check-manifest
+
+[check-manifest]
+ignore =
+ rxv/version.py
+
[flake8]
exclude = .cache,.git,.tox,.eggs,build,docs/*,*.egg-info
max-line-length = 100
diff --git a/setup.py b/setup.py
deleted file mode 100644
index f33d8c9..0000000
--- a/setup.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-from __future__ import absolute_import, division, print_function
-
-import sys
-
-from setuptools import find_packages, setup
-from setuptools.command.test import test as TestCommand
-
-
-class Tox(TestCommand):
-
- def finalize_options(self):
- TestCommand.finalize_options(self)
- self.test_args = []
- self.test_suite = True
-
- def run_tests(self):
- import tox
- errno = tox.cmdline(self.test_args)
- sys.exit(errno)
-
-
-setup(
- name='rxv',
- version='0.6.0',
- description='Automation Library for Yamaha RX-V473, RX-V573, RX-V673, RX-V773 receivers',
- long_description=open('README.rst').read(),
- author='Wojciech Bederski',
- url="https://github.com/wuub/rxv",
- license='BSD',
- author_email='github@wuub.net',
- packages=find_packages(),
- install_requires=['requests', 'defusedxml'],
- tests_require=['tox'],
- zip_safe=False,
- cmdclass={'test': Tox},
- classifiers=[
- "Development Status :: 4 - Beta",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: BSD License",
- "Operating System :: OS Independent",
- "Topic :: Software Development :: Libraries",
- "Topic :: Home Automation",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3.4",
- "Programming Language :: Python :: 3.5",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: Implementation :: PyPy"
- ]
-)
diff --git a/test-requirements.txt b/test-requirements.txt
deleted file mode 100644
index 30e880c..0000000
--- a/test-requirements.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-mock
-pytest>=2.9.2
-pytest-cov>=2.3.1
-pytest-timeout>=1.0.0
-testtools
-requests-mock
diff --git a/tests/test_feature_support.py b/tests/test_feature_support.py
index d4b791d..9e81288 100644
--- a/tests/test_feature_support.py
+++ b/tests/test_feature_support.py
@@ -1,12 +1,14 @@
from io import open
import requests_mock
-import testtools
+import unittest
+
import rxv
FAKE_IP = '10.0.0.0'
DESC_XML = 'http://%s/YamahaRemoteControl/desc.xml' % FAKE_IP
+CTRL_URL = 'http://%s/YamahaRemoteControl/ctrl' % FAKE_IP
def sample_content(name):
@@ -14,13 +16,13 @@ def sample_content(name):
return f.read()
-class TestFeaturesV675(testtools.TestCase):
+class TestFeaturesV675(unittest.TestCase):
@requests_mock.mock()
def setUp(self, m):
super(TestFeaturesV675, self).setUp()
m.get(DESC_XML, text=sample_content('rx-v675-desc.xml'))
- self.rec = rxv.RXV(FAKE_IP)
+ self.rec = rxv.RXV(CTRL_URL)
def test_supports_method(self):
rec = self.rec
diff --git a/tests/test_rxv.py b/tests/test_rxv.py
index f2f9447..9690369 100644
--- a/tests/test_rxv.py
+++ b/tests/test_rxv.py
@@ -1,12 +1,13 @@
from io import open
import requests_mock
-import testtools
+import unittest
import rxv
FAKE_IP = '10.0.0.0'
DESC_XML = 'http://%s/YamahaRemoteControl/desc.xml' % FAKE_IP
+CTRL_URL = 'http://%s/YamahaRemoteControl/ctrl' % FAKE_IP
def sample_content(name):
@@ -14,26 +15,23 @@ def sample_content(name):
return f.read()
-class TestRXV(testtools.TestCase):
+class TestRXV(unittest.TestCase):
@requests_mock.mock()
def test_basic_object(self, m):
m.get(DESC_XML, text=sample_content('rx-v675-desc.xml'))
- rec = rxv.RXV(FAKE_IP)
- self.assertEqual(
- rec.ctrl_url,
- 'http://%s/YamahaRemoteControl/ctrl' % FAKE_IP)
+ rec = rxv.RXV(CTRL_URL)
self.assertEqual(
rec.unit_desc_url,
'http://%s/YamahaRemoteControl/desc.xml' % FAKE_IP)
-class TestDesc(testtools.TestCase):
+class TestDesc(unittest.TestCase):
@requests_mock.mock()
def test_discover_zones(self, m):
m.get(DESC_XML, text=sample_content('rx-v675-desc.xml'))
- rec = rxv.RXV(FAKE_IP)
+ rec = rxv.RXV(CTRL_URL)
zones = rec.zone_controllers()
self.assertEqual(len(zones), 2, zones)
self.assertEqual(zones[0].zone, "Main_Zone")
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 22b4531..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,11 +0,0 @@
-[tox]
-envlist = py27,py34,py35,py36,py37,pypy
-skip_missing_interpreters = True
-
-[testenv]
-setenv =
- LANG=en_US.UTF-8
- PYTHONPATH = {toxinidir}
-deps =
- -r{toxinidir}/test-requirements.txt
-commands=py.test tests --timeout=30 --duration=10 --cov=rxv --cov-report html {posargs}