Skip to main content

Package for controlling VSSL's range of streaming amplifiers

Project description

vsslctrl

Package for controlling VSSL's range of streaming amplifiers.

Coverage

Tested on:

Model Software Version Note
A.3x p15305.016.3701

Home Assistant integration with basic functionality working on:

Model Software Version User Reported
A.1 p15265.033.3703 ✔️
A.3 p12013.141.3703 ✔️
A.3x p15305.016.3701
A.6x p15305.017.3701 ✔️

Testers Needed

I am looking for testers with any VSSL amplifier models, please get in touch if you interested in helping. vsslcontrolled@proton.me

Important

There should not be any VSSL Agent's running on the network. If you dont know what this is, then you can ignore this notice.

vsslctrl is not endorsed or affiliated with VSSL in any manner.

TODOs

  • Better test coverage
  • A.1(x) testing - e.g output settings
  • More controls - e.g IR Control

Basic Usage

vsslctrl needs to be running inside a asyncio event loop.

A.1 Example

import asyncio
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs

async def main():
  
  # Represents a physical VSSL amplifier
  vssl = Vssl(DeviceModels.A1)
  a1 = vssl.add_zone('192.168.1.10')

  # Connect and initialise zone.
  await vssl.initialise()

  """Control Examples"""
  # Print device name
  print(a1.settings.name)
  # Set volume to 25%
  a1.volume = 25
  # Pause
  a1.pause()
  # Print track name
  print(a1.track.name)

  # Shutdown and disconnect all zones
  await vssl.shutdown()

asyncio.run(main())

A.3x Example

import asyncio
from vsslctrl import Vssl, DeviceModels, Zone, ZoneIDs

async def main():
	
  # Represents a physical VSSL amplifier
  vssl = Vssl(DeviceModels.A3X)

  # Add each you wish to control
  zone1 = vssl.add_zone('192.168.1.10', ZoneIDs.ZONE_1)
  zone2 = vssl.add_zone('192.168.1.11', ZoneIDs.ZONE_2)
  zone3 = vssl.add_zone('192.168.1.12', ZoneIDs.ZONE_3)
  #... up to 6 zones for A.6(x)

  # Connect and initialise zones.
  await vssl.initialise()

  """Control Examples"""
  # Print zone1 name
  print(zone1.settings.name)
  # Set zone2 volume to 25%
  zone2.volume = 25
  # Pause zone3
  zone3.pause()
  # or zone3.transport.pause()
  # Print zone1 track name
  print(zone1.track.name)


  # Shutdown and disconnect all zones
  await vssl.shutdown()


asyncio.run(main())

Device Discovery Helper

You can discover VSSL devices on the network using mDNS / Bonjour if you have the zeroconf package installed.

This uses airplay service string _airplay._tcp.local., therefore airplay needs to available and will not work across VLANs without other provisions.

Note: This is designed to be a helper and its not recommended to be used for the initialization of the VSSL class.

import asyncio
from vsslctrl import Vssl

async def main():
  
  print(await Vssl.discover())

  """
    {
        'XXXXXXXXXXXX': [
            {
                'host': '192.168.168.25',
                'name': 'Living Room',
                'model': 'A1x',
                'mac_addr': 'AA:BB:CC:DD:EE:FF',
                'zone_id': '7',
                'serial': 'XXXXXXXXXXXX'
            }
        ]
    }

  """

asyncio.run(main())

API Functionality

Most functionality is achieved via getters and setters of the two main classes Vssl, Zone.

The classes will update the physical VSSL device when setting a property and once feedback has been received, the classes internal state will be updated. For example:

# Setting the zone name
zone1.settings.name = 'Living Room'
>>> 'Old Zone Name'

# Printing zone name
print(zone1.settings.name)
>>> 'Living Room'

Important in the above example, zone1.settings.name won't be set to its new value until after the VSSL device has changed the name and the Zone class has received confirmation feedback. If you need to wait for the value change, you can await a [property_name]_CHANGE events as below:

from vsslctrl.settings import ZoneSettings
# Setting the zone name and wait for feedback
future_name = vssl.event_bus.future(ZoneSettings.Events.NAME_CHANGE, zone1.id)
zone1.settings.name = 'Bathroom' 
# Helper to await a future with timeout
new_name = await vssl.event_bus.wait_future(future_name)
# Printing zone name
print(new_name)
>>> 'Bathroom'
# or
print(zone1.settings.name)
>>> 'Bathroom'

API Reference

DeviceModels

A device model has to be passed to VSSL so it knows internally what features are supported by the device. This might be removed in the future if we can differentiate different models from the API.

Property Description
A1X A.1x
A3X A.3x
A6X A.6x
A1 A1
A3 A3
A6 A6

ZoneIDs

A ZoneIDs must be passed to each zone you which to control and it must match the zone on the VSSL device.

If you are unsure of your ZoneIDs you could use the discovery helper to find out the correct mapping.

Property Description A.1(x) A.3(x) A.6(x)
A1 ✔️
ZONE_1 Zone 1 ✔️ ✔️
ZONE_2 Zone 2 ✔️ ✔️
ZONE_3 Zone 3 ✔️ ✔️
ZONE_4 Zone 4 ✔️
ZONE_5 Zone 5 ✔️
ZONE_6 Zone 6 ✔️

Vssl

Property Description Type
sw_version Software version str readonly
serial Serial number str readonly
model Device Model int readonly
reboot() Reboot all zones func
factory_reset() Factory reset device func
"""Example"""
# Reboot all zones
vssl.reboot()
# Do a factory reset (reset all settings)
vssl.factory_reset()

Vssl.settings

Property Description Type Model: Default
name Device name str
bus_1_name Name of Bus 1 str
  • A.1: Optical Input
  • A.3x/6x: Not Used
bus_2_name Name of Bus 2 str
  • A.1: Coax Input
  • A.3x/6x: Optical Input
bluetooth Bluetooth enabled / disabled bool
bluetooth_toggle() Toggle Bluetooth func
"""Example"""
# Setting device name
vssl.settings.name = 'My House'
# Setting bus 2 name
vssl.settings.bus_2_name = 'Optical Input'
# Enable Bluetooth
vssl.settings.bluetooth = True
# Toggle Bluetooth
vssl.settings.bluetooth_toggle()

Vssl.settings.power

Property Description Type Values
state Power state int readonly VsslPowerSettings.States
adaptive Power adaptive bool
"""Example"""
# Setting power adaptive
vssl.settings.power.adaptive = True

Zone

Property Description Type Values
id Zone number / ID int readonly ZoneIDs
host IP address str readonly
volume Volume int 0...100
volume_raise([step=1]) Raise volume by step func step: int 1...100
volume_lower([step=1]) Lower volume by step func step: int 1...100
mute Volume muted bool
mute_toggle() Mute / Unmute func
play() Play func
stop() Stop func
pause() Pause func
next() Next track func
prev() Begining of track or previous track func
reboot() Reboot zone func
play_url([url], [all_zones]) Play a URL func url: str, all_zones: bool
"""Examples"""
# Set volume to 50%
zone1.volume = 50
# Raise volume by 5%
zone1.volume_raise(5)
# Mute
zone1.mute = True
# Toggle mute
zone1.mute_toggle()
# Pause transport
zone1.pause()
# Next track
zone1.next()
# Play a URL on this zone1
zone1.play_url('http://soundbible.com/grab.php?id=2217&type=mp3')
# Play a URL on all zones
zone1.play_url('http://soundbible.com/grab.php?id=2217&type=mp3', True)

Zone.transport

A VSSL amplifier can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.

Property Description Type Values
state Transport state. i.e Play, Stop, Pause int ZoneTransport.States
play() Play func
stop() Stop func
pause() Pause func
next() Next track func
prev() Begining of track or previous track func
is_playing Is the zone playing bool readonly
is_stopped Is the zone stopped bool readonly
is_pasued Is the zone pasued bool readonly
is_repeat Repeat state. i.e all, one, off int readonly ZoneTransport.Repeat
is_shuffle Is shuffle enabled bool readonly
has_next Is the next button enabled bool readonly
has_prev Is the prev button enabled bool readonly
"""Example"""
# Pause the stream
zone1.transport.pause()
# or
zone1.transport.state = ZoneTransport.States.PAUSE

Zone.track

  • Not all sources have complete metadata - missing value will be set to defaults.
  • Airplay track progress is not available.
Property Description Type Values
title Title str readonly
album Album str readonly
artist Artist str readonly
genre Genre str readonly
duration Length in miliseconds (ms) int readonly
progress Current position in miliseconds (ms) int readonly
cover_art_url URL to cover art str readonly
source Track source e.g Spotify int readonly TrackMetadata.Sources
url URL of file or track str readonly

Zone.input

Property Description Type Values
source Change input source.
Source to be played out the zones speakers
int InputRouter.Sources
priority Change input priority.
Stream or local to have precedence
int InputRouter.Priorities

A.1(x) Source Routing Order

A.1 and A.1x don't support manually changing the input source. Instead a fixed source routing order is used:

  1. Optical Input
  2. Coaxial Input
  3. Analog Input

Input InputRouter.Priorities still apply.

Input Priority / Precedence

InputRouter.Priorities Priority Order
STREAM
  1. Stream
  2. Party Zone
  3. Bus 1
  4. Bus 2
  5. Optical
  6. Coaxial
  7. Analog
LOCAL
  1. Bus 1
  2. Bus 2
  3. Optical
  4. Coaxial
  5. Analog
  6. Stream
  7. Party Zone
"""Example"""
# Change zone 1 to listen to analog input 4
zone1.input.source = InputRouter.Sources.ANALOG_IN_4

# Change zone 1 to perfer local inputs over stream
zone1.input.priority = InputRouter.Priorities.LOCAL

Zone.group

Unsupported on X series amplifiers.

Property Description Type Values
source Zone ID of group master / source int readonly ZoneIDs
is_master This zone is the group master bool readonly
add_member() Add zone to group / create group func ZoneIDs
remove_member() Remove zone from group func ZoneIDs
dissolve() Dissolve group / remove all members func
leave() Leave the group if a member func
is_party_zone_member Member of Party Zone bool
"""Examples"""
# Add group 2 to a group with zone 1 as master
zone1.group.add_member(ZoneIDs.ZONE_2)
# Remove zone 2 from group
zone2.group.leave() # or
zone1.group.remove_member(ZoneIDs.ZONE_2)
# If zone 1 is a master, remove all members
zone1.group.dissolve()
# Add zone to the party zone group
zone1.group.is_party_zone_member = True
# Toggle Party Zone Membership
zone1.group.is_party_zone_member_toggle()

Zone.analog_output

Property Description Type Values Default
source Where the AO is routed from. i.e stream, optical input or off int AnalogOutput.Sources Off
is_fixed_volume Fix the output volume. Output wont respond to volume control bool False
is_fixed_volume_toggle() Toggle fixed volume func
"""Examples"""
# Change analog output of zone1 to be outputting the optical input
zone1.analog_output.source = AnalogOutput.Sources.OPTICAL_IN

# Change analog output of zone1 to be outputting the zone 2 source (whatever zone 2 is using as a source)
zone1.analog_output.source = AnalogOutput.Sources.ZONE_2

# Fix the analog output volume. 
zone1.analog_output.is_fixed_volume = True

Zone.settings

Property Description Type Values Default
name Name str
disabled Disable the zone bool False
disabled_toggle() disable / enable func
mono Set output to mono or stereo int ZoneSettings.StereoMono Stereo
mono_toggle() Toggle mono or stereo func
"""Examples"""
# Set name
zone1.settings.name = 'Living Room'
# Disable Zone
zone1.disabled = True
# Toggle mono output
zone1.mono_toggle()

Zone.settings.analog_input

Property Description Type Values Default
name Name str
fixed_gain Fix the input gain to a specific value int 0...100 0 is disabled or variable gain
"""Examples"""
# Change zone1 analog input name
zone1.settings.analog_input.name = 'BluRay Player'

# Fix zone1 analog input gain to 50%.
zone1.settings.analog_input.fixed_gain = 50

Zone.settings.volume

Property Description Type Values Default
default_on Default on volume int 0...100 0 is disabled
max_left Max volume left channel int 0...100 75
max_right Max volume right channel int 0...100 75
"""Examples"""
# Set default on volume to 50%
zone1.settings.volume.default_on = 50
# Set maximum volume for left channel to 75%
zone1.settings.volume.default_on = 75

Zone.settings.eq

Property Description Type Values Default
enabled Enable / disable EQ bool False

EQ to be set in decibel using a range -10dB to +10dB

Property Description Type Values Default
hz60_db 60Hz int -10...10 0
hz200_db 200Hz int -10...10 0
hz500_db 500Hz int -10...10 0
khz1_db 1kHz int -10...10 0
khz4_db 4kHz int -10...10 0
khz8_db 8kHz int -10...10 0
khz15_db 15kHz int -10...10 0
"""Examples"""
# Set 1kHz EQ to -2
zone1.settings.eq.khz1_db = -2

Zone.settings.subwoofer

  • A.1 and A.1x only
  • Set 0 for full frequency range
Property Description Type Values Default
crossover Set "sub out" crossover frequency from 50-200Hz. int 0 or 50...200 0
"""Examples"""
# Set subwoofer ouput crossover to 100hz
zone1.settings.subwoofer.crossover = 100

Credit

Thanks to @dj-jam for the continued testing.

The VSSL API was reverse engineered using Wireshark, VSSLs native "legacy" iOS app and their deprecated vsslagent.

Motivation for this project was to integrate VSSLs amplifiers into Home Assistant and have control over different subnets (not mDNS dependant)

Known Issues & Limitiations

  • Not tested on A.1x or original A series range of amplifiers (testers welcome)
  • VSSL can not start a stream except for playing a URL directly. This is a limitation of the hardware itself.
  • Not all sources set the volume to 0 when the zone is muted
  • Airplay Zone.track.progress is not available.
  • Cant stop a URL playback, feedback is worng at least
  • VSSL likes to cache old track metadata. For example when playing a URL after Spotify, often the device will respond with the previous (Spotify) tracks metadata
  • stop() is intended to disconnect the client and pause the stream. Doesn’t always function this way, depending on stream source
  • Occasionally a zones might stop responding to certain commands, issuing the reboot command generally corrects

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vsslctrl-0.1.13.dev1.tar.gz (56.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vsslctrl-0.1.13.dev1-py3-none-any.whl (53.1 kB view details)

Uploaded Python 3

File details

Details for the file vsslctrl-0.1.13.dev1.tar.gz.

File metadata

  • Download URL: vsslctrl-0.1.13.dev1.tar.gz
  • Upload date:
  • Size: 56.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.0 CPython/3.9.6

File hashes

Hashes for vsslctrl-0.1.13.dev1.tar.gz
Algorithm Hash digest
SHA256 ec4dff600e780e4eac2a9a213809aebc8b66460e8b7f0573eaabee1423b31770
MD5 5e10148725b0b476b84f02790153c719
BLAKE2b-256 56f4f4dd67135ad67b74745790a9616be9ef7c6fb728318f4639500f3cbfd39c

See more details on using hashes here.

File details

Details for the file vsslctrl-0.1.13.dev1-py3-none-any.whl.

File metadata

  • Download URL: vsslctrl-0.1.13.dev1-py3-none-any.whl
  • Upload date:
  • Size: 53.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.0 CPython/3.9.6

File hashes

Hashes for vsslctrl-0.1.13.dev1-py3-none-any.whl
Algorithm Hash digest
SHA256 249331da607b7c9914b74f7c5c0df05bb7201cbbae30f6b6d47aede914691aa7
MD5 c996f129a1e049368ad8e65278e9678e
BLAKE2b-256 09144a345da9707d749af95a47b28a30e99c36ccd25097489807af729da80e3c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page