Skip to content

Commit

Permalink
Merge pull request #4 from andrewsayre/dev
Browse files Browse the repository at this point in the history
v0.3.1
  • Loading branch information
andrewsayre authored Apr 8, 2019
2 parents 0cbf082 + 381542a commit 1246f9b
Show file tree
Hide file tree
Showing 19 changed files with 122 additions and 15 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,7 @@ venv.bak/
.mypy_cache/

# pycharm
.idea/
.idea/

# vscode
.vscode/
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"python.pythonPath": ".venv\\Scripts\\python.exe",
"[python]": {
"editor.rulers": [79]
}
}
1 change: 0 additions & 1 deletion pyheos/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""pyheos - a library for interacting with HEOS devices."""

from . import const
from .dispatch import Dispatcher
from .heos import Heos
Expand Down
26 changes: 23 additions & 3 deletions pyheos/command.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Define the HEOS command module."""
from typing import Sequence, Tuple
from typing import Optional, Sequence, Tuple

from . import const
from .player import HeosNowPlayingMedia
Expand All @@ -15,10 +15,30 @@ def __init__(self, connection):
async def heart_beat(self, *, raise_for_result=False) -> bool:
"""Perform heart beat command."""
response = await self._connection.command(
const.COMMAND_HEART_BEAT, raise_for_result,
const.COMMAND_HEART_BEAT, None,
raise_for_result=raise_for_result)
return response.result

async def check_account(self) -> Optional[str]:
"""Return the logged in username."""
response = await self._connection.command(
const.COMMAND_ACCOUNT_CHECK, None, True)
if response.has_message('signed_in'):
return response.get_message('un')
return None

async def sign_in(self, username: str, password: str):
"""Sign in to the HEOS account using the provided credential."""
params = {
'un': username,
'pw': password
}
await self._connection.command(const.COMMAND_SIGN_IN, params, True)

async def sign_out(self):
"""Sign out of the HEOS account."""
await self._connection.command(const.COMMAND_SIGN_OUT, None, True)

async def register_for_change_events(
self, enable=True, *, raise_for_result=False) -> bool:
"""Enable or disable change event notifications."""
Expand Down Expand Up @@ -77,7 +97,7 @@ async def get_volume(self, player_id: int) -> int:
}
response = await self._connection.command(
const.COMMAND_GET_VOLUME, params, raise_for_result=True)
return int(response.get_message('level'))
return int(float(response.get_message('level')))

async def set_volume(self, player_id: int, level: int,
*, raise_for_result=False) -> bool:
Expand Down
5 changes: 4 additions & 1 deletion pyheos/const.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Define consts for the pyheos package."""

__title__ = "pyheos"
__version__ = "0.3.0"
__version__ = "0.3.1"

CLI_PORT = 1255
DEFAULT_TIMEOUT = 10.0
Expand Down Expand Up @@ -208,6 +208,9 @@
# System commands
COMMAND_REGISTER_FOR_CHANGE_EVENTS = "system/register_for_change_events"
COMMAND_HEART_BEAT = "system/heart_beat"
COMMAND_ACCOUNT_CHECK = "system/check_account"
COMMAND_SIGN_IN = "system/sign_in"
COMMAND_SIGN_OUT = "system/sign_out"

# Events
EVENT_PLAYER_STATE_CHANGED = "event/player_state_changed"
Expand Down
24 changes: 24 additions & 0 deletions pyheos/heos.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ def __init__(self, host: str, *,
self._players_loaded = False
self._music_sources = {} # type: Dict[int, HeosSource]
self._music_sources_loaded = False
self._signed_in_username = None # type: str

async def connect(self, *, auto_reconnect=False,
reconnect_delay: float = const.DEFAULT_RECONNECT_DELAY):
"""Connect to the CLI."""
await self._connection.connect(auto_reconnect=auto_reconnect,
reconnect_delay=reconnect_delay)
self._signed_in_username = \
await self._connection.commands.check_account()

async def disconnect(self):
"""Disconnect from the CLI."""
Expand All @@ -49,8 +52,19 @@ async def _handle_event(self, event: HeosResponse) -> bool:
if event.command == const.EVENT_SOURCES_CHANGED \
and self._music_sources_loaded:
await self.get_music_sources(refresh=True)
if event.command == const.EVENT_USER_CHANGED:
self._signed_in_username = event.get_message('un') \
if event.has_message("signed_in") else None
return True

async def sign_in(self, username: str, password: str):
"""Sign-in to the HEOS account on the device directly connected."""
await self._connection.commands.sign_in(username, password)

async def sign_out(self):
"""Sign-out of the HEOS account on the device directly connected."""
await self._connection.commands.sign_out()

async def get_players(self, *, refresh=False) -> Dict[int, HeosPlayer]:
"""Get available players."""
# get players and pull initial state
Expand Down Expand Up @@ -131,3 +145,13 @@ def music_sources(self) -> Dict[int, HeosSource]:
def connection_state(self):
"""Get the state of the connection."""
return self._connection.state

@property
def is_signed_in(self) -> bool:
"""Return True if the HEOS accuont is signed in."""
return bool(self._signed_in_username)

@property
def signed_in_username(self) -> Optional[str]:
"""Return the signed-in username."""
return self._signed_in_username
2 changes: 1 addition & 1 deletion pyheos/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ async def event_update(self, event: HeosResponse,
elif event.command == const.EVENT_PLAYER_NOW_PLAYING_CHANGED:
await self.refresh_now_playing_media()
elif event.command == const.EVENT_PLAYER_VOLUME_CHANGED:
self._volume = int(event.get_message('level'))
self._volume = int(float(event.get_message('level')))
self._is_muted = event.get_message('mute') == 'on'
elif event.command == const.EVENT_REPEAT_MODE_CHANGED:
self._repeat = event.get_message('repeat')
Expand Down
4 changes: 4 additions & 0 deletions pyheos/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def get_message(self, key: str) -> Any:
if self._message:
return self._message.get(key)

def has_message(self, key: str) -> bool:
"""Determine if the key within the message."""
return self._message and key in self._message

def get_player_id(self) -> int:
"""Get the player_id from the message."""
return int(self._message['pid'])
Expand Down
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ async def start(self):
self._handle_connection, '127.0.0.1', const.CLI_PORT)

self.register(const.COMMAND_HEART_BEAT, None, 'system.heart_beat')
self.register(const.COMMAND_ACCOUNT_CHECK, None,
'system.check_account')
self.register(const.COMMAND_GET_PLAYERS, None, 'player.get_players')
self.register(const.COMMAND_GET_PLAY_STATE, None,
'player.get_play_state')
Expand Down
1 change: 0 additions & 1 deletion tests/fixtures/event.user_changed.json

This file was deleted.

1 change: 1 addition & 0 deletions tests/fixtures/event.user_changed_signed_in.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "event/user_changed", "message": "signed_in&un=example@example.com"}}
1 change: 1 addition & 0 deletions tests/fixtures/event.user_changed_signed_out.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "event/user_changed", "message": "signed_out&un=Unknown"}}
2 changes: 1 addition & 1 deletion tests/fixtures/player.get_volume.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"heos": {"result": "success", "command": "player/get_volume", "message": "pid={player_id}&level=36&sequence={sequence}"}}
{"heos": {"result": "success", "command": "player/get_volume", "message": "pid={player_id}&level=36.0&sequence={sequence}"}}
1 change: 1 addition & 0 deletions tests/fixtures/system.check_account.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "system/check_account", "result": "success", "message": "signed_in&un=example@example.com"}}
1 change: 1 addition & 0 deletions tests/fixtures/system.check_account_logged_out.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "system/check_account", "result": "success", "message": "signed_out"}}
1 change: 1 addition & 0 deletions tests/fixtures/system.sign_in.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "system/sign_in", "result": "success", "message": "signed_in&un=example@example.com"}}
1 change: 1 addition & 0 deletions tests/fixtures/system.sign_in_failure.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "system/sign_in", "result": "fail", "message": "eid=10&text=User not found"}}
1 change: 1 addition & 0 deletions tests/fixtures/system.sign_out.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"heos": {"command": "system/sign_out", "result": "success", "message": "signed_out"}}
52 changes: 46 additions & 6 deletions tests/test_heos.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,22 @@ async def test_connect(mock_device):
assert len(mock_device.connections) == 1
connection = mock_device.connections[0]
assert connection.is_registered_for_events

assert heos.is_signed_in
assert heos.signed_in_username == "example@example.com"
await heos.disconnect()


@pytest.mark.asyncio
async def test_connect_not_logged_in(mock_device, heos):
"""Test signed-in status shows correctly when logged out."""
mock_device.register(const.COMMAND_ACCOUNT_CHECK, None,
'system.check_account_logged_out', replace=True)
heos = Heos('127.0.0.1')
await heos.connect()
assert not heos.is_signed_in
assert not heos.signed_in_username


@pytest.mark.asyncio
async def test_heart_beat(mock_device):
"""Test heart beat fires at interval."""
Expand Down Expand Up @@ -339,7 +351,7 @@ async def handler(player_id: int, event: str):
# Write event through mock device
event_to_raise = (await get_fixture("event.player_volume_changed")) \
.replace("{player_id}", str(player.player_id)) \
.replace("{level}", '50') \
.replace("{level}", '50.0') \
.replace("{mute}", 'on')
await mock_device.write_event(event_to_raise)

Expand Down Expand Up @@ -612,20 +624,28 @@ async def handler(group_id: int, event: str):

@pytest.mark.asyncio
async def test_user_changed_event(mock_device, heos):
"""Test user changed fires dispatcher."""
"""Test user changed fires dispatcher and updates logged in user."""
signal = asyncio.Event()

async def handler(event: str):
assert event == const.EVENT_USER_CHANGED
signal.set()
heos.dispatcher.connect(const.SIGNAL_CONTROLLER_EVENT, handler)

# Write event through mock device
event_to_raise = await get_fixture("event.user_changed")
# Test signed out event
event_to_raise = await get_fixture("event.user_changed_signed_out")
await mock_device.write_event(event_to_raise)
await signal.wait()
assert not heos.is_signed_in
assert not heos.signed_in_username

# Wait until the signal is set
# Test signed in event
signal.clear()
event_to_raise = await get_fixture("event.user_changed_signed_in")
await mock_device.write_event(event_to_raise)
await signal.wait()
assert heos.is_signed_in
assert heos.signed_in_username == "example@example.com"


@pytest.mark.asyncio
Expand Down Expand Up @@ -678,3 +698,23 @@ async def test_get_favorites(mock_device, heos):
assert fav.playable
assert fav.name == 'Thumbprint Radio'
assert fav.type == const.TYPE_STATION


@pytest.mark.asyncio
async def test_sign_in_and_out(mock_device, heos):
"""Test the sign in and sign out methods."""
data = {'un': "example@example.com", 'pw': 'example'}
# Test sign-in failure
mock_device.register(const.COMMAND_SIGN_IN, data, 'system.sign_in_failure')
with pytest.raises(CommandError) as e_info:
await heos.sign_in("example@example.com", "example")
assert str(e_info.value.error_text) == "User not found"

# Test sign-in success
mock_device.register(const.COMMAND_SIGN_IN, data, 'system.sign_in',
replace=True)
await heos.sign_in("example@example.com", "example")

# Test sign-out
mock_device.register(const.COMMAND_SIGN_OUT, None, 'system.sign_out')
await heos.sign_out()

0 comments on commit 1246f9b

Please sign in to comment.