pax_global_header00006660000000000000000000000064142227365110014515gustar00rootroot0000000000000052 comment=db5686baa301b6f6d72fd298c9c987f779df7ac6 ssaenger-pyws66i-db5686b/000077500000000000000000000000001422273651100152735ustar00rootroot00000000000000ssaenger-pyws66i-db5686b/.coveragerc000066400000000000000000000005441422273651100174170ustar00rootroot00000000000000[run] source = pyws66i [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError ssaenger-pyws66i-db5686b/.travis.yml000066400000000000000000000002741422273651100174070ustar00rootroot00000000000000language: python python: - "3.8" - "3.9" cache: pip before_install: - pip3 install pytest pytest-cov - pip3 install coveralls script: - py.test --cov after_success: - coverallsssaenger-pyws66i-db5686b/LICENSE.txt000066400000000000000000000020501422273651100171130ustar00rootroot00000000000000MIT License Copyright (c) 2017 Egor Tsinko Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ssaenger-pyws66i-db5686b/README.md000066400000000000000000000042161422273651100165550ustar00rootroot00000000000000## Status [![Build Status](https://app.travis-ci.com/ssaenger/pyws66i.svg?branch=master)](https://app.travis-ci.com/ssaenger/pyws66i)[![Coverage Status](https://coveralls.io/repos/github/ssaenger/pyws66i/badge.svg?branch=master)](https://coveralls.io/github/ssaenger/pyws66i?branch=master) # pyws66i Python3 interface implementation for [Soundavo WS66i amplifier](https://www.soundavo.com/products/ws-66i). ## Notes This is a 6-zone amplifier that is a direct upgrade from ws66i 6-zone amplifier. This is a fork off of [pymonoprice](https://github.com/etsinko/pymonoprice) that replaces the serial protocol for telnet. It is intended to be used with [Home-Assistant](http://home-assistant.io). ## Usage ```python from pyws66i import get_ws66i # Get a connection using the IP address of the WS66i amplifier ws66i = get_ws66i('192.168.1.123') # Open a connection try: ws66i.open() except ConnectionError: # Handle exception # Valid zones are 11-16 for the main WS66i amplifier zone_status = ws66i.zone_status(11) # Print zone status print('Zone Number = {}'.format(zone_status.zone)) print('Power is {}'.format('On' if zone_status.power else 'Off')) print('Mute is {}'.format('On' if zone_status.mute else 'Off')) print('Public Anouncement Mode is {}'.format('On' if zone_status.pa else 'Off')) print('Do Not Disturb Mode is {}'.format('On' if zone_status.do_not_disturb else 'Off')) print('Volume = {}'.format(zone_status.volume)) print('Treble = {}'.format(zone_status.treble)) print('Bass = {}'.format(zone_status.bass)) print('Balance = {}'.format(zone_status.balance)) print('Source = {}'.format(zone_status.source)) print('Keypad is {}'.format('connected' if zone_status.keypad else 'disconnected')) # Turn off zone #11 ws66i.set_power(11, False) # Mute zone #12 ws66i.set_mute(12, True) # Set volume for zone #13 ws66i.set_volume(13, 15) # Set source 1 for zone #14 ws66i.set_source(14, 1) # Set treble for zone #15 ws66i.set_treble(15, 10) # Set bass for zone #16 ws66i.set_bass(16, 7) # Set balance for zone #11 ws66i.set_balance(11, 3) # Restore zone #11 to it's original state ws66i.restore_zone(zone_status) # Done. Close the connection ws66i.close() ``` ssaenger-pyws66i-db5686b/pyws66i/000077500000000000000000000000001422273651100166225ustar00rootroot00000000000000ssaenger-pyws66i-db5686b/pyws66i/__init__.py000066400000000000000000000246261422273651100207450ustar00rootroot00000000000000import logging from telnetlib import Telnet import socket from functools import wraps from threading import RLock _LOGGER = logging.getLogger(__name__) TIMEOUT = 0.8 # Number of seconds before telnet operation timeout class ZoneStatus(object): def __init__( self, zone: int, # (11 - 16) pa: bool, power: bool, mute: bool, do_not_disturb: bool, volume: int, # (0 - 38) treble: int, # (0 - 14) where 0 -> -7, 14 -> +7 bass: int, # (0 - 14) where 0 -> -7, 14 -> +7 balance: int, # 00 - left, 10 - center, 20 right source: int, # (1 - 6) keypad: bool, ): self.zone = zone self.pa = bool(pa) self.power = bool(power) self.mute = bool(mute) self.do_not_disturb = bool(do_not_disturb) self.volume = volume self.treble = treble self.bass = bass self.balance = balance self.source = source self.keypad = bool(keypad) str(self) def __str__(self): return f""" zone: {self.zone}, pa: {self.pa}, power: {self.power}, mute: {self.mute}, do_not_disturb: {self.do_not_disturb}, volume: {self.volume}, treble: {self.treble}, bass: {self.bass}, balance: {self.balance}, source: {self.source}, keypad: {self.keypad} """ @classmethod def from_string(cls, match): if not match: return None return ZoneStatus(*[int(m) for m in match.groups()]) class WS66i(object): """ WS66i amplifier interface """ def open(self): """ Open a connection to the Telnet server. Must be the first call. """ raise NotImplementedError def close(self): """ Close the connection to the Telnet server. Open() will need to be called again to communicate with the server. """ raise NotImplementedError def zone_status(self, zone: int): """ Get the structure representing the status of the zone :param zone: zone 11..16, 21..26, 31..36 :return: status of the zone or None. If None is returned then an error was occured. This is most likely due to the amp being turned off. It will attempt to re-establish a connection if a connection was previously present. """ raise NotImplementedError def set_power(self, zone: int, power: bool): """ Turn zone on or off :param zone: zone 11..16, 21..26, 31..36 :param power: True to turn on, False to turn off """ raise NotImplementedError def set_mute(self, zone: int, mute: bool): """ Mute zone on or off :param zone: zone 11..16, 21..26, 31..36 :param mute: True to mute, False to unmute """ raise NotImplementedError def set_volume(self, zone: int, volume: int): """ Set volume for zone :param zone: zone 11..16, 21..26, 31..36 :param volume: integer from 0 to 38 inclusive """ raise NotImplementedError def set_treble(self, zone: int, treble: int): """ Set treble for zone :param zone: zone 11..16, 21..26, 31..36 :param treble: integer from 0 to 14 inclusive, where 0 is -7 treble and 14 is +7 """ raise NotImplementedError def set_bass(self, zone: int, bass: int): """ Set bass for zone :param zone: zone 11..16, 21..26, 31..36 :param bass: integer from 0 to 14 inclusive, where 0 is -7 bass and 14 is +7 """ raise NotImplementedError def set_balance(self, zone: int, balance: int): """ Set balance for zone :param zone: zone 11..16, 21..26, 31..36 :param balance: integer from 0 to 20 inclusive, where 0 is -10(left), 0 is center and 20 is +10 (right) """ raise NotImplementedError def set_source(self, zone: int, source: int): """ Set source for zone :param zone: zone 11..16, 21..26, 31..36 :param source: integer from 0 to 6 inclusive """ raise NotImplementedError def restore_zone(self, status: ZoneStatus): """ Restores zone to it's previous state :param status: zone state to restore """ raise NotImplementedError # Helpers def _format_zone_status_request(zone: int) -> bytes: return "?{}\r".format(zone).encode() def _format_set_power(zone: int, power: bool) -> bytes: return "<{}PR{}\r".format(zone, "01" if power else "00").encode() def _format_set_mute(zone: int, mute: bool) -> bytes: return "<{}MU{}\r".format(zone, "01" if mute else "00").encode() def _format_set_volume(zone: int, volume: int) -> bytes: volume = int(max(0, min(volume, 38))) return "<{}VO{:02}\r".format(zone, volume).encode() def _format_set_treble(zone: int, treble: int) -> bytes: treble = int(max(0, min(treble, 14))) return "<{}TR{:02}\r".format(zone, treble).encode() def _format_set_bass(zone: int, bass: int) -> bytes: bass = int(max(0, min(bass, 14))) return "<{}BS{:02}\r".format(zone, bass).encode() def _format_set_balance(zone: int, balance: int) -> bytes: balance = max(0, min(balance, 20)) return "<{}BL{:02}\r".format(zone, balance).encode() def _format_set_source(zone: int, source: int) -> bytes: source = int(max(1, min(source, 6))) return "<{}CH{:02}\r".format(zone, source).encode() def get_ws66i(host_name: str, host_port=8080): """ Return synchronous version of the WS66i interface :param host_name: host name, i.e. '192.168.1.123' :param host_port: must be 8080 :return: synchronous implementation of WS66i interface """ lock = RLock() def synchronized(func): @wraps(func) def wrapper(*args, **kwargs): with lock: return func(*args, **kwargs) return wrapper class WS66iSync(WS66i): def __init__(self, host_name: str, host_port: int): self._host_name = host_name self._host_port = host_port self._connected = False self._telnet = Telnet() def __del__(self): self._telnet.close() def open(self): try: self._telnet.open(self._host_name, self._host_port, TIMEOUT) self._connected = True except (TimeoutError, OSError, socket.timeout, socket.gaierror) as err: raise ConnectionError from err @synchronized def close(self): self._telnet.close() self._connected = False def _process_request(self, request: bytes, expect_zone=None): """ :param request: request that is sent to the WS66i :param exepct_zone: The zone to fetch data from :return: Match object or None """ _LOGGER.debug('Sending "%s"', request) try: self._telnet.write(request) if expect_zone is not None: # Exepct a regex string to prevent unsynchronized behavior when # multiple clients communicate simultaneously with the WS66i expect_str = f"({expect_zone})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)" resp = self._telnet.expect([expect_str.encode()], timeout=TIMEOUT) _LOGGER.debug('Received "%s"', str(resp[1])) return resp[1] except UnboundLocalError: _LOGGER.error('Bad Write Request') except EOFError: _LOGGER.error('Expect str "%s" produced no result', expect_str) except (TimeoutError, socket.timeout, BrokenPipeError) as error: _LOGGER.error('Timed-Out with exception: %s', repr(error)) return None @synchronized def zone_status(self, zone: int): # Check if socket is open before reading zone status # Did the caller called open first? if not self._connected: _LOGGER.debug('Connection needed first') return None if not self._telnet.get_socket(): # The connection should be established, but an error was # encountered (most likely amp was turned off) # Attempt to re-establish the connection. try: self.open() except ConnectionError: return None zone_status = ZoneStatus.from_string(self._process_request(_format_zone_status_request(zone), zone)) if zone_status is None: # Amp is most likely turned off. Close the connection. # Future calls to zone_status will try to reconnect. self._telnet.close() return zone_status @synchronized def set_power(self, zone: int, power: bool): self._process_request(_format_set_power(zone, power)) @synchronized def set_mute(self, zone: int, mute: bool): self._process_request(_format_set_mute(zone, mute)) @synchronized def set_volume(self, zone: int, volume: int): self._process_request(_format_set_volume(zone, volume)) @synchronized def set_treble(self, zone: int, treble: int): self._process_request(_format_set_treble(zone, treble)) @synchronized def set_bass(self, zone: int, bass: int): self._process_request(_format_set_bass(zone, bass)) @synchronized def set_balance(self, zone: int, balance: int): self._process_request(_format_set_balance(zone, balance)) @synchronized def set_source(self, zone: int, source: int): self._process_request(_format_set_source(zone, source)) @synchronized def restore_zone(self, status: ZoneStatus): self.set_power(status.zone, status.power) self.set_mute(status.zone, status.mute) self.set_volume(status.zone, status.volume) self.set_treble(status.zone, status.treble) self.set_bass(status.zone, status.bass) self.set_balance(status.zone, status.balance) self.set_source(status.zone, status.source) return WS66iSync(host_name, host_port) ssaenger-pyws66i-db5686b/setup.cfg000066400000000000000000000000501422273651100171070ustar00rootroot00000000000000[metadata] description-file = README.md ssaenger-pyws66i-db5686b/setup.py000077500000000000000000000041701422273651100170120ustar00rootroot00000000000000#!/usr/bin/env python import os import sys VERSION = "v1.1" try: from setuptools import setup except ImportError: from distutils.core import setup if sys.argv[-1] == "publish": os.system("python setup.py sdist upload") sys.exit() license = """ MIT License Copyright (c) 2022 Shawn Saenger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() setup( name="pyws66i", version=VERSION, description="Python API for talking to Soundavo's WS66i 6-zone amplifier using the telnet protocol", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/ssaenger/pyws66i", download_url="https://github.com/ssaenger/pyws66i/archive/{}.tar.gz".format(VERSION), author="Shawn Saenger", author_email="shawnsaenger@gmail.com", license="MIT", packages=["pyws66i"], classifiers=[ "Development Status :: 5 - Production/Stable", 'Programming Language :: Python :: 3.8', "Programming Language :: Python :: 3.9", "License :: OSI Approved :: MIT License", ], zip_safe=True, ) ssaenger-pyws66i-db5686b/tests/000077500000000000000000000000001422273651100164355ustar00rootroot00000000000000ssaenger-pyws66i-db5686b/tests/__init__.py000066400000000000000000000000341422273651100205430ustar00rootroot00000000000000""" Init file for tests """ ssaenger-pyws66i-db5686b/tests/test_ws66i.py000066400000000000000000000435501422273651100210330ustar00rootroot00000000000000import unittest from unittest import TestCase, mock import re import socket from pyws66i import get_ws66i, ZoneStatus, TIMEOUT class TestZoneStatus(TestCase): def test_zone_status_broken(self): self.assertIsNone(ZoneStatus.from_string(None)) class TestWs66i(TestCase): def setUp(self): self.patcher = mock.patch('pyws66i.Telnet') self.mock_telnet = self.patcher.start() self.telnet_instance = self.mock_telnet.return_value self.ws66i = get_ws66i("168.192.1.123") self.ws66i.open() self.telnet_instance.open.assert_called_once() def tearDown(self): self.ws66i.close() self.patcher.stop() def test_close(self): # ----------- test close is called----------- # call self.ws66i.close() self.telnet_instance.close.assert_called_once() def test_bad_open(self): # ----------- test expect raises TimeoutError ----------- # call self.telnet_instance.open.side_effect = TimeoutError() self.assertRaises(ConnectionError, self.ws66i.open) # ----------- test expect raises OSError ----------- # call self.telnet_instance.open.side_effect = OSError() self.assertRaises(ConnectionError, self.ws66i.open) # ----------- test expect raises socket.timeout ----------- # call self.telnet_instance.open.side_effect = socket.timeout() self.assertRaises(ConnectionError, self.ws66i.open) # ----------- test expect raises socket.gaierror ----------- # call self.telnet_instance.open.side_effect = socket.gaierror() self.assertRaises(ConnectionError, self.ws66i.open) def test_zone_status(self): # ----------- Test good format ----------- # setup zone = 11 expected_write = f'?{zone}\r'.encode() expected_string_coded = "1100010000131112100401".encode() expected_pattern_coded = f"({zone})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)".encode() # call self.telnet_instance.expect.return_value = [None, re.search(expected_pattern_coded, expected_string_coded), None] status = self.ws66i.zone_status(zone) # check self.telnet_instance.write.assert_called_with(expected_write) self.telnet_instance.expect.assert_called_with([expected_pattern_coded], timeout=TIMEOUT) self.assertEqual(zone, status.zone) self.assertFalse(status.pa) self.assertTrue(status.power) self.assertFalse(status.mute) self.assertFalse(status.do_not_disturb) self.assertEqual(13, status.volume) self.assertEqual(11, status.treble) self.assertEqual(12, status.bass) self.assertEqual(10, status.balance) self.assertEqual(4, status.source) self.assertTrue(status.keypad) # ----------- test expect raises EOFError ----------- # Clear Mock self.telnet_instance.reset_mock() # setup zone = 11 expected_write = f'?{zone}\r'.encode() expected_pattern_coded = f"({zone})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)".encode() # call self.telnet_instance.expect.side_effect = EOFError() status = self.ws66i.zone_status(zone) # check self.telnet_instance.get_socket.assert_called() self.telnet_instance.write.assert_called() self.telnet_instance.expect.assert_called_with([expected_pattern_coded], timeout=TIMEOUT) self.telnet_instance.close.assert_called_once() self.assertIsNone(status) # ----------- test fails open ----------- # Clear Mock self.telnet_instance.reset_mock() # call self.telnet_instance.get_socket.return_value = None self.telnet_instance.open.side_effect = TimeoutError() status = self.ws66i.zone_status(zone) # check self.telnet_instance.get_socket.assert_called() self.telnet_instance.open.assert_called_once() self.telnet_instance.write.assert_not_called() self.telnet_instance.expect.assert_not_called() self.telnet_instance.close.assert_not_called() self.assertIsNone(status) # ----------- test expect raises TimeoutError ----------- # Clear Mock self.telnet_instance.reset_mock() # call self.telnet_instance.open.side_effect = None self.telnet_instance.get_socket.return_value = None self.telnet_instance.expect.side_effect = TimeoutError() status = self.ws66i.zone_status(zone) # check self.telnet_instance.get_socket.assert_called() self.telnet_instance.open.assert_called_once() self.telnet_instance.write.assert_called_once() self.telnet_instance.expect.assert_called_once() self.telnet_instance.close.assert_called_once() self.assertIsNone(status) # ----------- test expect raises socket.timeout ----------- # Clear Mock self.telnet_instance.reset_mock() # call self.telnet_instance.get_socket.return_value = None self.telnet_instance.expect.side_effect = socket.timeout() status = self.ws66i.zone_status(zone) # check self.telnet_instance.get_socket.assert_called() self.telnet_instance.open.assert_called_once() self.telnet_instance.write.assert_called_once() self.telnet_instance.expect.assert_called_once() self.telnet_instance.close.assert_called_once() self.assertIsNone(status) # ----------- test expect raises UnboundLocalError ----------- # Clear Mock self.telnet_instance.reset_mock() # call self.telnet_instance.get_socket.return_value = None self.telnet_instance.expect.side_effect = UnboundLocalError() status = self.ws66i.zone_status(zone) # check self.telnet_instance.get_socket.assert_called() self.telnet_instance.open.assert_called_once() self.telnet_instance.write.assert_called_once() self.telnet_instance.expect.assert_called_once() self.assertIsNone(status) # ----------- test expect raises BrokenPipeError ----------- # Clear Mock self.telnet_instance.reset_mock() # call self.telnet_instance.get_socket.return_value = None self.telnet_instance.expect.side_effect = BrokenPipeError() status = self.ws66i.zone_status(zone) # check self.telnet_instance.get_socket.assert_called() self.telnet_instance.open.assert_called_once() self.telnet_instance.write.assert_called_once() self.telnet_instance.expect.assert_called_once() self.assertIsNone(status) # ----------- test connection re-established, success ----------- # call # This is a way to check if a method was called or not... self.telnet_instance.reset_mock() # setup zone = 11 expected_write = f'?{zone}\r'.encode() expected_string_coded = "1100010000131112100401".encode() expected_pattern_coded = f"({zone})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)".encode() # call self.telnet_instance.expect.side_effect = None self.telnet_instance.expect.return_value = [None, re.search(expected_pattern_coded, expected_string_coded), None] status = self.ws66i.zone_status(zone) # check self.telnet_instance.get_socket.assert_called_once() self.telnet_instance.open.assert_called_once() self.telnet_instance.write.assert_called_once() self.telnet_instance.expect.assert_called_once() self.telnet_instance.close.assert_not_called() # check self.assertIsNotNone(status) # ----------- test zone_status with closed connection ----------- # Clear Mock self.telnet_instance.reset_mock() # setup # A way to check that "write" is not called prev_write_count = self.telnet_instance.write.call_count # call self.ws66i.close() status = self.ws66i.zone_status(zone) # check self.telnet_instance.close.assert_called_once() self.telnet_instance.get_socket.assert_not_called() self.telnet_instance.open.assert_not_called() self.assertEqual(prev_write_count, self.telnet_instance.write.call_count) self.telnet_instance.expect.assert_not_called() self.assertIsNone(status) def test_set_power(self): # ----------- test 2nd arg is True ----------- # setup zone = 12 expected_write = f'<{zone}PR01\r'.encode() # call self.ws66i.set_power(zone, True) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is "True" ----------- # call self.ws66i.set_power(zone, "True") # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is 1 ----------- # call self.ws66i.set_power(zone, 1) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is False ----------- # setup expected_write = f'<{zone}PR00\r'.encode() # call self.ws66i.set_power(zone, False) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is None ----------- # call self.ws66i.set_power(zone, None) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is 0 ----------- # call self.ws66i.set_power(zone, 0) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is "" ----------- # call self.ws66i.set_power(zone, "") # check self.telnet_instance.write.assert_called_with(expected_write) def test_set_mute(self): # ----------- test 2nd arg is True ----------- # setup zone = 13 expected_write = f'<{zone}MU01\r'.encode() # call self.ws66i.set_mute(zone, True) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is "True" ----------- # call self.ws66i.set_mute(zone, "True") # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is 1 ----------- # call self.ws66i.set_mute(zone, 1) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is False ----------- # setup expected_write = f'<{zone}MU00\r'.encode() # call self.ws66i.set_mute(zone, False) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is None ----------- # call self.ws66i.set_mute(zone, None) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is 0 ----------- # call self.ws66i.set_mute(zone, 0) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test 2nd arg is "" ----------- # call self.ws66i.set_mute(zone, "") # check self.telnet_instance.write.assert_called_with(expected_write) def test_set_volume(self): # ----------- test vol 1 ----------- # setup zone = 14 expected_write = f'<{zone}VO01\r'.encode() # call self.ws66i.set_volume(zone, 1) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test above 38 ----------- # setup expected_write = f'<{zone}VO38\r'.encode() # call self.ws66i.set_volume(zone, 100) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test vol below 0 ----------- # setup expected_write = f'<{zone}VO00\r'.encode() # call self.ws66i.set_volume(zone, -10) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test vol 22 ----------- # setup expected_write = f'<{zone}VO22\r'.encode() # call self.ws66i.set_volume(zone, 22) # check self.telnet_instance.write.assert_called_with(expected_write) def test_set_treble(self): # ----------- test treble 1 ----------- # setup zone = 15 expected_write = f'<{zone}TR01\r'.encode() # call self.ws66i.set_treble(zone, 1) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test treble above 14 ----------- # setup expected_write = f'<{zone}TR14\r'.encode() # call self.ws66i.set_treble(zone, 100) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test treble below 0 ----------- # setup expected_write = f'<{zone}TR00\r'.encode() # call self.ws66i.set_treble(zone, -10) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test treble 5 ----------- # setup expected_write = f'<{zone}TR05\r'.encode() # call self.ws66i.set_treble(zone, 5) # check self.telnet_instance.write.assert_called_with(expected_write) def test_set_bass(self): # ----------- test bass 1 ----------- # setup zone = 15 expected_write = f'<{zone}BS01\r'.encode() # call self.ws66i.set_bass(zone, 1) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test bass above 14 ----------- # setup expected_write = f'<{zone}BS14\r'.encode() # call self.ws66i.set_bass(zone, 100) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test bass below 0 ----------- # setup expected_write = f'<{zone}BS00\r'.encode() # call self.ws66i.set_bass(zone, -10) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test bass 11 ----------- # setup expected_write = f'<{zone}BS05\r'.encode() # call self.ws66i.set_bass(zone, 5) # check self.telnet_instance.write.assert_called_with(expected_write) def test_set_balance(self): # ----------- test balance 1 ----------- # setup zone = 15 expected_write = f'<{zone}BL01\r'.encode() # call self.ws66i.set_balance(zone, 1) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test balance above 20 ----------- # setup expected_write = f'<{zone}BL20\r'.encode() # call self.ws66i.set_balance(zone, 100) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test balance below 0 ----------- # setup expected_write = f'<{zone}BL00\r'.encode() # call self.ws66i.set_balance(zone, -10) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test balance 16 ----------- # setup expected_write = f'<{zone}BL16\r'.encode() # call self.ws66i.set_balance(zone, 16) # check self.telnet_instance.write.assert_called_with(expected_write) def test_set_source(self): # ----------- test source 1 ----------- # setup zone = 16 expected_write = f'<{zone}CH01\r'.encode() # call self.ws66i.set_source(zone, 1) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test source above 6 ----------- # setup expected_write = f'<{zone}CH06\r'.encode() # call self.ws66i.set_source(zone, 100) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test source below 0 ----------- # setup expected_write = f'<{zone}CH01\r'.encode() # call self.ws66i.set_source(zone, -10) # check self.telnet_instance.write.assert_called_with(expected_write) # ----------- test source 6 ----------- # setup expected_write = f'<{zone}CH06\r'.encode() # call self.ws66i.set_source(zone, 16) # check self.telnet_instance.write.assert_called_with(expected_write) def test_restore_zone(self): # setup expected_zone = 11 expected_string_coded = "1100010000131112100401".encode() expected_pattern_coded = f"({expected_zone})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)".encode() ew_pr = f'<{expected_zone}PR01\r'.encode() ew_mu = f'<{expected_zone}MU00\r'.encode() ew_vo = f'<{expected_zone}VO13\r'.encode() ew_tr = f'<{expected_zone}TR11\r'.encode() ew_bs = f'<{expected_zone}BS12\r'.encode() ew_bl = f'<{expected_zone}BL10\r'.encode() ew_ch = f'<{expected_zone}CH04\r'.encode() expected_list = [mock.call(ew_pr), mock.call(ew_mu), mock.call(ew_vo), mock.call(ew_tr), mock.call(ew_bs), mock.call(ew_bl), mock.call(ew_ch)] # call and check zone_status = ZoneStatus.from_string(re.search(expected_pattern_coded, expected_string_coded)) self.ws66i.restore_zone(zone_status) self.assertTrue(self.telnet_instance.write.call_args_list == expected_list) if __name__ == "__main__": unittest.main()