├── .github └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml ├── setup.py ├── src └── steelseries_sonar_py │ ├── __init__.py │ ├── exceptions.py │ └── sonar.py └── test.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | workflow_dispatch: # Allow running the workflow manually from the GitHub UI 4 | push: 5 | branches: 6 | - 'master' # Run the workflow when pushing to the main branch 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - '*' # Run the workflow for all pull requests 12 | 13 | jobs: 14 | build: 15 | name: Build distribution 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Check out src from Git 20 | uses: actions/checkout@v4 21 | - name: Get history and tags for SCM versioning to work 22 | run: | 23 | git fetch --prune --unshallow 24 | if [ -z "$(git tag)" ]; then 25 | echo "No tags found, skipping fetch tags" 26 | else 27 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* || (echo "Failed to fetch tags" && exit 1) 28 | fi 29 | - name: Set up Python 30 | uses: actions/setup-python@v4 31 | with: 32 | python-version: "3.x" 33 | - name: Install pypa/build 34 | run: python3 -m pip install build --user 35 | - name: Build a binary wheel and a source tarball 36 | run: python3 -m build 37 | - name: Store the distribution packages 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: python-package-distributions 41 | path: dist/ 42 | 43 | publish-to-testpypi: 44 | name: Publish Python distribution to TestPyPI 45 | needs: 46 | - build 47 | runs-on: ubuntu-latest 48 | if: github.event_name == 'push' 49 | 50 | environment: 51 | name: testpypi 52 | url: https://test.pypi.org/p/steelseries-sonar-py 53 | 54 | permissions: 55 | id-token: write # IMPORTANT: mandatory for trusted publishing 56 | 57 | steps: 58 | - name: Download all the dists 59 | uses: actions/download-artifact@v3 60 | with: 61 | name: python-package-distributions 62 | path: dist/ 63 | - name: Publish distribution to TestPyPI 64 | uses: pypa/gh-action-pypi-publish@release/v1 65 | with: 66 | repository-url: https://test.pypi.org/legacy/ 67 | 68 | publish-to-pypi: 69 | name: Publish to PyPI 70 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 71 | needs: 72 | - build 73 | runs-on: ubuntu-latest 74 | environment: 75 | name: pypi 76 | url: https://pypi.org/p/steelseries-sonar-py 77 | permissions: 78 | id-token: write # IMPORTANT: mandatory for trusted publishing 79 | steps: 80 | - name: Download all the dists 81 | uses: actions/download-artifact@v3 82 | with: 83 | name: python-package-distributions 84 | path: dist/ 85 | - name: Publish distribution to PyPI 86 | uses: pypa/gh-action-pypi-publish@release/v1 87 | 88 | github-release: 89 | name: >- 90 | Sign the Python distribution with Sigstore 91 | and upload them to GitHub Release 92 | needs: 93 | - publish-to-pypi 94 | runs-on: ubuntu-latest 95 | 96 | permissions: 97 | contents: write # IMPORTANT: mandatory for making GitHub Releases 98 | id-token: write # IMPORTANT: mandatory for sigstore 99 | 100 | steps: 101 | - name: Download all the dists 102 | uses: actions/download-artifact@v3 103 | with: 104 | name: python-package-distributions 105 | path: dist/ 106 | - name: Sign the dists with Sigstore 107 | uses: sigstore/gh-action-sigstore-python@v2.1.1 108 | with: 109 | inputs: >- 110 | ./dist/*.tar.gz 111 | ./dist/*.whl 112 | - name: Create GitHub Release 113 | env: 114 | GITHUB_TOKEN: ${{ github.token }} 115 | run: >- 116 | gh release create 117 | '${{ github.ref_name }}' 118 | --repo '${{ github.repository }}' 119 | --notes "" 120 | - name: Upload artifact signatures to GitHub Release 121 | env: 122 | GITHUB_TOKEN: ${{ github.token }} 123 | # Upload to GitHub Release using the `gh` CLI. 124 | # `dist/` contains the built packages, and the 125 | # sigstore-produced signatures and certificates. 126 | run: >- 127 | gh release upload 128 | '${{ github.ref_name }}' dist/** 129 | --repo '${{ github.repository }}' 130 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![publish](https://github.com/Mark7888/steelseries-sonar-py/actions/workflows/publish.yml/badge.svg?event=push)](https://github.com/Mark7888/steelseries-sonar-py/actions/workflows/publish.yml) 2 | [![Downloads](https://static.pepy.tech/badge/steelseries-sonar-py)](https://pepy.tech/project/steelseries-sonar-py) 3 | 4 | # SteelSeries Sonar Python API 5 | 6 | **Streamer mode currently in development!** 7 | 8 | ## Overview 9 | 10 | This Python package provides a convenient interface for interacting with the SteelSeries Sonar application API. 11 | The Sonar application allows users to control and display volumes for various audio channels. 12 | 13 | ## Installation 14 | 15 | To use this package, follow these steps: 16 | 17 | 1. Install the package using pip: 18 | 19 | ```bash 20 | pip install steelseries-sonar-py 21 | ``` 22 | 23 | 2. Import the `Sonar` class in your Python script or application: 24 | 25 | ```python 26 | from steelseries_sonar_py import Sonar 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Initializing the Sonar Object 32 | 33 | The Sonar class accepts two optional parameters during initialization: 34 | `streamer_mode`: Set to True to use streamer mode (default is False). 35 | `app_data_path`: Specify a custom path for the SteelSeries Engine 3 coreProps.json file 36 | (default is the default installation path: `C:\\ProgramData\\SteelSeries\\SteelSeries Engine 3\\coreProps.json`). 37 | 38 | ```python 39 | sonar = Sonar(app_data_path="C:\\path\\to\\coreProps.json") 40 | ``` 41 | or 42 | ```python 43 | sonar = Sonar(app_data_path="C:\\path\\to\\coreProps.json", streamer_mode=True) 44 | ``` 45 | 46 | ### Streamer Mode 47 | 48 | The SteelSeries Sonar Python API supports streamer mode, which allows users to manage two separate sliders: `streaming` and `monitoring`. These sliders enable fine-tuned control over different audio channels. 49 | 50 | To check if the streamer mode is enabled, use: 51 | 52 | ```python 53 | is_streaming = sonar.is_streamer_mode() 54 | print("Is Streamer Mode:", is_streaming) 55 | ``` 56 | 57 | To enable or disable streamer mode, use: 58 | 59 | ```python 60 | # Enable streamer mode 61 | sonar.set_streamer_mode(True) 62 | 63 | # Disable streamer mode 64 | sonar.set_streamer_mode(False) 65 | ``` 66 | 67 | ### Retrieving Volume Information 68 | 69 | Retrieve information about the current volume settings for all channels: 70 | 71 | ```python 72 | volume_data = sonar.get_volume_data() 73 | print(volume_data) 74 | ``` 75 | 76 | ### Setting Volume for a Channel 77 | 78 | Set the volume for a specific channel. The `channel` parameter should be one of the following: 79 | `master`, `game`, `chatRender`, `media`, `aux`, `chatCapture`. The `volume` parameter should be a float between 0 and 1. 80 | Additionally, an optional `streamer_slider` parameter can be provided, with values "streaming" (default) or "monitoring": 81 | 82 | ```python 83 | channel = "master" 84 | volume = 0.75 85 | streamer_slider = "streaming" # or "monitoring" 86 | 87 | result = sonar.set_volume(channel, volume, streamer_slider=streamer_slider) 88 | print(result) 89 | ``` 90 | 91 | ### Muting/Unmuting a Channel 92 | 93 | Toggle mute status for a specific channel. The `channel` parameter should be one of the following: 94 | `master`, `game`, `chatRender`, `media`, `aux`, `chatCapture`. The `muted` parameter should be a boolean indicating whether to mute (`True`) or unmute (`False`) the channel. 95 | Additionally, an optional `streamer_slider` parameter can be provided, with values "streaming" (default) or "monitoring": 96 | 97 | ```python 98 | channel = "game" 99 | muted = True 100 | streamer_slider = "monitoring" 101 | 102 | result = sonar.mute_channel(channel, muted, streamer_slider=streamer_slider) 103 | print(result) 104 | ``` 105 | 106 | ### Chatmix 107 | 108 | Retrieve chat-mix data: 109 | 110 | ```python 111 | chatmix_data = sonar.get_chat_mix_data() 112 | print(chatmix_data) 113 | ``` 114 | 115 | Set chat-mix value between `-1 and 1` to focus sound from the `game` or `chatRender` channel: 116 | 117 | ```python 118 | result = sonar.set_chat_mix(0.5) 119 | print(result) 120 | ``` 121 | 122 | ## Exceptions 123 | 124 | The package introduces a set of exceptions that might be raised during usage. 125 | It is advisable to handle these exceptions accordingly in your code. 126 | You can import them from `steelseries_sonar_py.exceptions`. Here is the list of potential exceptions: 127 | 128 | - `EnginePathNotFoundError`: Raised when SteelSeries Engine 3 is not installed or not in the default location. 129 | - `ServerNotAccessibleError`: Raised when the SteelSeries server is not accessible. Provides the HTTP status code. 130 | - `SonarNotEnabledError`: Raised when SteelSeries Sonar is not enabled. 131 | - `ServerNotReadyError`: Raised when SteelSeries Sonar is not ready. 132 | - `ServerNotRunningError`: Raised when SteelSeries Sonar is not running. 133 | - `WebServerAddressNotFoundError`: Raised when the web server address is not found. 134 | - `ChannelNotFoundError`: Raised when the specified channel is not found. 135 | - `InvalidVolumeError`: Raised when an invalid volume value is provided. 136 | - `InvalidMixVolumeError`: Raised when an invalid mix volume value is provided. 137 | - `SliderNotFoundError`: Raised when an unknown slider name is provided as `streamer_slider` value. 138 | 139 | ## Example 140 | 141 | Here is a complete example demonstrating the usage of the SteelSeries Sonar Python API: 142 | 143 | ```python 144 | from steelseries_sonar_py import Sonar 145 | from steelseries_sonar_py.exceptions import EnginePathNotFoundError 146 | 147 | # Initialize Sonar object 148 | try: 149 | sonar = Sonar(app_data_path="C:\\path\\to\\coreProps.json") 150 | except EnginePathNotFoundError: 151 | print("Engine not found!") 152 | quit() 153 | 154 | # Retrieve volume data 155 | volume_data = sonar.get_volume_data() 156 | print("Volume Data:", volume_data) 157 | 158 | # Set volume for the 'master' channel 159 | channel = "master" 160 | volume = 0.8 161 | streamer_slider = "streaming" 162 | result = sonar.set_volume(channel, volume, streamer_slider=streamer_slider) 163 | print(f"Set volume for {channel}:", result) 164 | 165 | # Mute the 'game' channel 166 | channel = "game" 167 | muted = True 168 | streamer_slider = "monitoring" 169 | result = sonar.mute_channel(channel, muted, streamer_slider=streamer_slider) 170 | print(f"Mute {channel}:", result) 171 | 172 | # Retrieve chat-mix data 173 | chatmix_data = sonar.get_chat_mix_data() 174 | print("Chatmix Data:", chatmix_data) 175 | 176 | # Set chat-mix value 177 | result = sonar.set_chat_mix(0.5) 178 | print("Set Chatmix:", result) 179 | ``` 180 | 181 | ## Special Thanks 182 | 183 | Thanks to all contributors who made this package possible - [wex](https://github.com/wex/sonar-rev) for figuring out the API, [TotalPanther317](https://github.com/TotalPanther317/steelseries-sonar-py) for understanding streamer mode and [cookie](https://github.com/cookie0o) for features like chat mix and streamer mode detection. Grateful for their efforts! 184 | 185 | This documentation now reflects the latest changes and additions to the SteelSeries Sonar Python API. 186 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=60", "setuptools-scm>=8.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | local_scheme = "no-local-version" 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md', 'r', encoding='utf-8') as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='steelseries-sonar-py', 8 | author='Mark7888', 9 | author_email='l.mark7888@gmail.com', 10 | description='Simple Python wrapper for the SteelSeries Sonar API', 11 | keywords='steelseries, sonar, volume, control, sonar-api', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/Mark7888/steelseries-sonar-py', 15 | project_urls={ 16 | 'Documentation': 'https://github.com/Mark7888/steelseries-sonar-py/blob/master/README.md', 17 | 'Bug Reports': 'https://github.com/Mark7888/steelseries-sonar-py/issues', 18 | 'Source Code': 'https://github.com/Mark7888/steelseries-sonar-py', 19 | }, 20 | package_dir={'': 'src'}, 21 | packages=setuptools.find_packages(where='src'), 22 | install_requires=[ 23 | 'requests >= 2.31.0', 24 | ], 25 | classifiers=[ 26 | # see https://pypi.org/classifiers/ 27 | 'Development Status :: 5 - Production/Stable', 28 | 29 | 'Intended Audience :: Developers', 30 | 'Topic :: Software Development :: Build Tools', 31 | 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.1', 34 | 'Programming Language :: Python :: 3.2', 35 | 'Programming Language :: Python :: 3.3', 36 | 'Programming Language :: Python :: 3.4', 37 | 'Programming Language :: Python :: 3.5', 38 | 'Programming Language :: Python :: 3.6', 39 | 'Programming Language :: Python :: 3.7', 40 | 'Programming Language :: Python :: 3.8', 41 | 'Programming Language :: Python :: 3.9', 42 | 'Programming Language :: Python :: 3.10', 43 | 'Programming Language :: Python :: 3.11', 44 | 'Programming Language :: Python :: 3.12', 45 | 'Programming Language :: Python :: 3 :: Only', 46 | 'License :: OSI Approved :: MIT License', 47 | 'Operating System :: Microsoft :: Windows', 48 | ], 49 | python_requires='>=3', 50 | ) 51 | -------------------------------------------------------------------------------- /src/steelseries_sonar_py/__init__.py: -------------------------------------------------------------------------------- 1 | from .sonar import * 2 | -------------------------------------------------------------------------------- /src/steelseries_sonar_py/exceptions.py: -------------------------------------------------------------------------------- 1 | class EnginePathNotFoundError(Exception): 2 | def __str__(self): 3 | return "SteelSeries Engine 3 not installed or not in the default location!" 4 | 5 | 6 | class ServerNotAccessibleError(Exception): 7 | def __init__(self, status_code): 8 | self.status_code = status_code 9 | 10 | def __str__(self): 11 | return f"SteelSeries server not accessible! Status code: {self.status_code}" 12 | 13 | 14 | class SonarNotEnabledError(Exception): 15 | def __str__(self): 16 | return "SteelSeries Sonar is not enabled!" 17 | 18 | 19 | class ServerNotReadyError(Exception): 20 | def __str__(self): 21 | return "SteelSeries Sonar is not ready yet!" 22 | 23 | 24 | class ServerNotRunningError(Exception): 25 | def __str__(self): 26 | return "SteelSeries Sonar is not running!" 27 | 28 | 29 | class WebServerAddressNotFoundError(Exception): 30 | def __str__(self): 31 | return "Web server address not found" 32 | 33 | 34 | class ChannelNotFoundError(Exception): 35 | def __init__(self, channel): 36 | self.channel = channel 37 | 38 | def __str__(self): 39 | return f"Channel '{self.channel}' not found" 40 | 41 | class SliderNotFoundError(Exception): 42 | def __init__(self, slider): 43 | self.slider = slider 44 | 45 | def __str__(self): 46 | return f"Slider '{self.slider}' not found" 47 | 48 | 49 | class InvalidVolumeError(Exception): 50 | def __init__(self, volume): 51 | self.volume = volume 52 | 53 | def __str__(self): 54 | return f"Invalid volume '{self.volume}'! Value must be between 0 and 1!" 55 | 56 | class InvalidMixVolumeError(Exception): 57 | def __init__(self, mix_volume): 58 | self.mix_volume = mix_volume 59 | 60 | def __str__(self): 61 | return f"Invalid mix volume '{self.mix_volume}'! Value must be between -1 and 1!" 62 | -------------------------------------------------------------------------------- /src/steelseries_sonar_py/sonar.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import os 4 | 5 | from . import exceptions as ex 6 | 7 | 8 | 9 | class Sonar: 10 | # chatCapture = mic 11 | channel_names = ["master", "game", "chatRender", "media", "aux", "chatCapture"] 12 | streamer_slider_names = ["streaming", "monitoring"] 13 | volume_path = "/volumeSettings/classic" 14 | app_data_path = os.path.join( 15 | os.environ["ProgramData"], 16 | "SteelSeries", 17 | "SteelSeries Engine 3", 18 | "coreProps.json", 19 | ) 20 | 21 | def __init__(self, app_data_path=None, streamer_mode=None): 22 | requests.packages.urllib3.disable_warnings() 23 | 24 | if app_data_path is not None: 25 | self.app_data_path = app_data_path 26 | 27 | self.load_base_url() 28 | self.load_server_address() 29 | 30 | if streamer_mode is None: 31 | self.streamer_mode = self.is_streamer_mode() 32 | 33 | if self.streamer_mode: 34 | self.volume_path = "/volumeSettings/streamer" 35 | 36 | def is_streamer_mode(self): 37 | streamer_mode_data = requests.get(self.web_server_address + "/mode/", verify=False) 38 | if streamer_mode_data.status_code != 200: 39 | raise ex.ServerNotAccessibleError(streamer_mode_data.status_code) 40 | 41 | return json.loads(streamer_mode_data.text) == "stream" 42 | 43 | def set_streamer_mode(self, streamer_mode): 44 | if streamer_mode: 45 | mode = "stream" 46 | else: 47 | mode = "classic" 48 | 49 | url = f"{self.web_server_address}/mode/{mode}" 50 | streamer_mode_data = requests.put(url) 51 | if streamer_mode_data.status_code != 200: 52 | raise ex.ServerNotAccessibleError(streamer_mode_data.status_code) 53 | 54 | self.streamer_mode = json.loads(streamer_mode_data.text) == "stream" 55 | return self.streamer_mode 56 | 57 | def load_base_url(self): 58 | if not os.path.exists(self.app_data_path): 59 | raise ex.EnginePathNotFoundError() 60 | 61 | with open(self.app_data_path, "r") as rf: 62 | common_app_data = json.load(rf) 63 | self.base_url = f'https://{common_app_data["ggEncryptedAddress"]}' 64 | 65 | def load_server_address(self): 66 | app_data = requests.get(self.base_url + "/subApps", verify=False) 67 | if app_data.status_code != 200: 68 | raise ex.ServerNotAccessibleError(app_data.status_code) 69 | 70 | app_data_json = json.loads(app_data.text) 71 | 72 | if not app_data_json["subApps"]["sonar"]["isEnabled"]: 73 | raise ex.SonarNotEnabledError() 74 | 75 | if not app_data_json["subApps"]["sonar"]["isReady"]: 76 | raise ex.ServerNotReadyError() 77 | 78 | if not app_data_json["subApps"]["sonar"]["isRunning"]: 79 | raise ex.ServerNotRunningError() 80 | 81 | self.web_server_address = app_data_json["subApps"]["sonar"]["metadata"][ 82 | "webServerAddress" 83 | ] 84 | if self.web_server_address in ["", None, "null"]: 85 | raise ex.WebServerAddressNotFoundError() 86 | 87 | def get_volume_data(self): 88 | volume_info_url = self.web_server_address + self.volume_path 89 | 90 | volume_data = requests.get(volume_info_url) 91 | if volume_data.status_code != 200: 92 | raise ex.ServerNotAccessibleError(volume_data.status_code) 93 | volume_data_json = json.loads(volume_data.text) 94 | 95 | return volume_data_json 96 | 97 | def set_volume(self, channel, volume, streamer_slider="streaming"): 98 | if channel not in self.channel_names: 99 | raise ex.ChannelNotFoundError(channel) 100 | 101 | if self.streamer_mode and streamer_slider not in self.streamer_slider_names: 102 | raise ex.SliderNotFoundError(streamer_slider) 103 | 104 | if volume < 0 or volume > 1: 105 | raise ex.InvalidVolumeError(volume) 106 | 107 | full_volume_path = self.volume_path 108 | if self.streamer_mode: 109 | full_volume_path += f"/{streamer_slider}" 110 | 111 | url = f"{self.web_server_address}{full_volume_path}/{channel}/Volume/{json.dumps(volume)}" 112 | volume_data = requests.put(url) 113 | if volume_data.status_code != 200: 114 | raise ex.ServerNotAccessibleError(volume_data.status_code) 115 | 116 | return json.loads(volume_data.text) 117 | 118 | def mute_channel(self, channel, muted, streamer_slider="streaming"): 119 | if channel not in self.channel_names: 120 | raise ex.ChannelNotFoundError(channel) 121 | 122 | if self.streamer_mode and streamer_slider not in self.streamer_slider_names: 123 | raise ex.SliderNotFoundError(streamer_slider) 124 | 125 | muted = muted in [True, "true", "True", 1, "1"] 126 | 127 | full_volume_path = self.volume_path 128 | if self.streamer_mode: 129 | full_volume_path += f"/{streamer_slider}" 130 | 131 | mute_keyword = "isMuted" if self.streamer_mode else "Mute" 132 | 133 | url = f"{self.web_server_address}{full_volume_path}/{channel}/{mute_keyword}/{json.dumps(muted)}" 134 | mute_data = requests.put(url) 135 | if mute_data.status_code != 200: 136 | raise ex.ServerNotAccessibleError(mute_data.status_code) 137 | 138 | return json.loads(mute_data.text) 139 | 140 | def get_chat_mix_data(self): 141 | chat_mix_url = self.web_server_address + "/chatMix" 142 | 143 | chat_mix_data = requests.get(chat_mix_url) 144 | if chat_mix_data.status_code != 200: 145 | raise ex.ServerNotAccessibleError(chat_mix_data.status_code) 146 | 147 | return json.loads(chat_mix_data.text) 148 | 149 | def set_chat_mix(self, mix_volume): 150 | if mix_volume < -1 or mix_volume > 1: 151 | raise ex.InvalidMixVolumeError(mix_volume) 152 | 153 | url = f"{self.web_server_address}/chatMix?balance={json.dumps(mix_volume)}" 154 | chat_mix_data = requests.put(url) 155 | if chat_mix_data.status_code != 200: 156 | raise ex.ServerNotAccessibleError(chat_mix_data.status_code) 157 | 158 | return json.loads(chat_mix_data.text) 159 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from src.steelseries_sonar_py.sonar import Sonar 2 | from src.steelseries_sonar_py.exceptions import * 3 | 4 | import time 5 | 6 | def test_classic_mode(): 7 | try: 8 | sonar = Sonar() 9 | 10 | print("Disabling streamer mode: ", not sonar.set_streamer_mode(False)) 11 | 12 | volume_data = sonar.get_volume_data() 13 | print("Classic Mode - Volume Data:", volume_data) 14 | 15 | channel = "master" 16 | volume = 0.5 17 | result = sonar.set_volume(channel, volume) 18 | print(f"Classic Mode - Set volume for {channel}:", result) 19 | 20 | channel = "game" 21 | muted = True 22 | result = sonar.mute_channel(channel, muted) 23 | print(f"Classic Mode - Mute {channel}:", result) 24 | 25 | except EnginePathNotFoundError: 26 | print("Engine not found!") 27 | except ServerNotAccessibleError as e: 28 | print(f"Server not accessible, status code: {e.status_code}") 29 | 30 | def test_streamer_mode(): 31 | try: 32 | sonar = Sonar() 33 | 34 | print("Enabling streamer mode: ", sonar.set_streamer_mode(True)) 35 | 36 | for slider in ["streaming", "monitoring"]: 37 | volume_data = sonar.get_volume_data() 38 | print(f"Streamer Mode ({slider}) - Volume Data:", volume_data) 39 | 40 | channel = "master" 41 | volume = 0.5 42 | result = sonar.set_volume(channel, volume, streamer_slider=slider) 43 | print(f"Streamer Mode ({slider}) - Set volume for {channel}:", result) 44 | 45 | channel = "game" 46 | muted = True 47 | result = sonar.mute_channel(channel, muted, streamer_slider=slider) 48 | print(f"Streamer Mode ({slider}) - Mute {channel}:", result) 49 | 50 | except EnginePathNotFoundError: 51 | print("Engine not found!") 52 | except ServerNotAccessibleError as e: 53 | print(f"Server not accessible, status code: {e.status_code}") 54 | 55 | if __name__ == "__main__": 56 | test_classic_mode() 57 | print("\n\n--------------------------------\n\n") 58 | test_streamer_mode() 59 | --------------------------------------------------------------------------------