pax_global_header00006660000000000000000000000064146651577340014534gustar00rootroot0000000000000052 comment=a0cb2e8fed229e023fa43130cb5f2e44ca9bc28f pyControl4-1.2.1/000077500000000000000000000000001466515773400136125ustar00rootroot00000000000000pyControl4-1.2.1/.github/000077500000000000000000000000001466515773400151525ustar00rootroot00000000000000pyControl4-1.2.1/.github/workflows/000077500000000000000000000000001466515773400172075ustar00rootroot00000000000000pyControl4-1.2.1/.github/workflows/CI.yml000066400000000000000000000032711466515773400202300ustar00rootroot00000000000000# This is a basic workflow to help you get started with Actions name: CI # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: push: pull_request: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: name: Lint code with flake8 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.8 uses: actions/setup-python@v1 with: python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Setup flake8 annotations uses: rbialon/flake8-annotations@v1 - name: Lint with flake8 run: | pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 ./pyControl4 --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 ./pyControl4 --count --ignore=E501 --exit-zero --max-complexity=10 --max-line-length=127 --statistics black: name: Check code formatting with black # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 # Runs a single command using the runners shell - name: Black Code Formatter uses: lgeiger/black-action@v1.0.1 with: args: "./pyControl4 --check --diff" pyControl4-1.2.1/.github/workflows/codeql-analysis.yml000066400000000000000000000031351466515773400230240ustar00rootroot00000000000000name: "CodeQL" on: push: pull_request: # The branches below must be a subset of the branches above schedule: - cron: '0 18 * * 6' jobs: analyse: name: Analyse runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 # Override language selection by uncommenting this and choosing your languages # with: # languages: go, javascript, csharp, python, cpp, java # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 pyControl4-1.2.1/.github/workflows/pdoc.yml000066400000000000000000000030701466515773400206570ustar00rootroot00000000000000# This is a basic workflow to help you get started with Actions name: pdoc # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: push: branches: [ master ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" generate-documentation: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 - name: Install pdoc3 uses: BSFishy/pip-action@v1 with: # The packages to install from Pip requirements: requirements-dev.txt # Runs a set of commands using the runners shell - name: Run pdoc run: pdoc --html pyControl4/ --output-dir docs-temp/ --force - name: Copy HTML files to root of docs/ folder run: cp -r docs-temp/pyControl4/. docs/ && rm -r docs-temp/pyControl4/ - name: Commit files run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git add --all git diff-index --quiet HEAD || git commit -am "Update documentation" - name: Push changes uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} pyControl4-1.2.1/.github/workflows/pythonpublish.yml000066400000000000000000000015041466515773400226420ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: PyPI Release on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python setup.py sdist bdist_wheel twine upload dist/* pyControl4-1.2.1/.github/workflows/shiftleft-analysis.yml000066400000000000000000000026131466515773400235450ustar00rootroot00000000000000# This workflow integrates ShiftLeft Scan with GitHub's code scanning feature # ShiftLeft Scan is a free open-source security tool for modern DevOps teams # Visit https://slscan.io/en/latest/integrations/code-scan for help name: ShiftLeft Scan # This section configures the trigger for the workflow. Feel free to customize depending on your convention on: push: pull_request: # The branches below must be a subset of the branches above jobs: Scan-Build: # Scan runs on ubuntu, mac and windows runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 # Instructions # 1. Setup JDK, Node.js, Python etc depending on your project type # 2. Compile or build the project before invoking scan # Example: mvn compile, or npm install or pip install goes here # 3. Invoke ShiftLeft Scan with the github token. Leave the workspace empty to use relative url - name: Perform ShiftLeft Scan uses: ShiftLeftSecurity/scan-action@master env: WORKSPACE: "" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SCAN_AUTO_BUILD: true with: output: reports # Scan auto-detects the languages in your project. To override uncomment the below variable and set the type # type: credscan,java # type: python - name: Upload report uses: github/codeql-action/upload-sarif@v1 with: sarif_file: reports pyControl4-1.2.1/.gitignore000066400000000000000000000000711466515773400156000ustar00rootroot00000000000000.venv/ .vscode/ __pycache__/ login_info.py allitems.json pyControl4-1.2.1/LICENSE000066400000000000000000000261351466515773400146260ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pyControl4-1.2.1/README.md000066400000000000000000000046521466515773400151000ustar00rootroot00000000000000# pyControl4 [![PyPI version](https://badge.fury.io/py/pyControl4.svg)](https://badge.fury.io/py/pyControl4)[![Downloads](https://pepy.tech/badge/pycontrol4)](https://pepy.tech/project/pycontrol4) [![CI](https://github.com/lawtancool/pyControl4/workflows/CI/badge.svg)](https://github.com/lawtancool/pyControl4/actions?query=workflow%3ACI)[![pdoc](https://github.com/lawtancool/pyControl4/workflows/pdoc/badge.svg)](https://github.com/lawtancool/pyControl4/actions?query=workflow%3Apdoc)[![PyPI Release](https://github.com/lawtancool/pyControl4/workflows/PyPI%20Release/badge.svg)](https://github.com/lawtancool/pyControl4/actions?query=workflow%3A%22PyPI+Release%22) An asynchronous library to interact with Control4 systems through their built-in REST API. This is known to work on controllers with OS 2.10.1.544795-res and OS 3.0+. Auto-generated function documentation can be found at For those who are looking for a pre-built solution for controlling their devices, this library is implemented in the [official Home Assistant Control4 integration](https://www.home-assistant.io/integrations/control4/). ## Usage example ```python from pyControl4.account import C4Account from pyControl4.director import C4Director from pyControl4.light import C4Light import asyncio import json username = "" password = "" ip = "192.168.1.25" """Authenticate with Control4 account""" account = C4Account(username, password) asyncio.run(account.getAccountBearerToken()) """Get and print controller name""" accountControllers = asyncio.run(account.getAccountControllers()) print(accountControllers["controllerCommonName"]) """Get bearer token to communicate with controller locally""" director_bearer_token = asyncio.run( account.getDirectorBearerToken(accountControllers["controllerCommonName"]) )["token"] """Create new C4Director instance""" director = C4Director(ip, director_bearer_token) """Print all devices on the controller""" print(asyncio.run(director.getAllItemInfo())) """Create new C4Light instance""" light = C4Light(director, 253) """Ramp light level to 10% over 10000ms""" asyncio.run(light.rampToLevel(10, 10000)) """Print state of light""" print(asyncio.run(light.getState())) ``` ## Contributing Pull requests are welcome! Please lint your Python code with `flake8` and format it with [Black](https://pypi.org/project/black/). ## Disclaimer This library is not affiliated with or endorsed by Control4. pyControl4-1.2.1/docs/000077500000000000000000000000001466515773400145425ustar00rootroot00000000000000pyControl4-1.2.1/docs/account.html000066400000000000000000000533001466515773400170650ustar00rootroot00000000000000 pyControl4.account API documentation

Module pyControl4.account

Authenticates with the Control4 API, retrieves account and registered controller info, and retrieves a bearer token for connecting to a Control4 Director.

Classes

class C4Account (username, password, session: aiohttp.client.ClientSession = None)

Creates a Control4 account object.

Parameters

username - Control4 account username/email.

password - Control4 account password.

session - (Optional) Allows the use of an aiohttp.ClientSession object for all network requests. This session will not be closed by the library. If not provided, the library will open and close its own ClientSessions as needed.

Expand source code
class C4Account:
    def __init__(
        self,
        username,
        password,
        session: aiohttp.ClientSession = None,
    ):
        """Creates a Control4 account object.

        Parameters:
            `username` - Control4 account username/email.

            `password` - Control4 account password.

            `session` - (Optional) Allows the use of an `aiohttp.ClientSession` object for all network requests. This session will not be closed by the library.
            If not provided, the library will open and close its own `ClientSession`s as needed.
        """
        self.username = username
        self.password = password
        self.session = session

    async def __sendAccountAuthRequest(self):
        """Used internally to retrieve an account bearer token. Returns the entire
        JSON response from the Control4 auth API.
        """
        dataDictionary = {
            "clientInfo": {
                "device": {
                    "deviceName": "pyControl4",
                    "deviceUUID": "0000000000000000",
                    "make": "pyControl4",
                    "model": "pyControl4",
                    "os": "Android",
                    "osVersion": "10",
                },
                "userInfo": {
                    "applicationKey": APPLICATION_KEY,
                    "password": self.password,
                    "userName": self.username,
                },
            }
        }
        if self.session is None:
            async with aiohttp.ClientSession() as session:
                with async_timeout.timeout(10):
                    async with session.post(
                        AUTHENTICATION_ENDPOINT, json=dataDictionary
                    ) as resp:
                        await checkResponseForError(await resp.text())
                        return await resp.text()
        else:
            with async_timeout.timeout(10):
                async with self.session.post(
                    AUTHENTICATION_ENDPOINT, json=dataDictionary
                ) as resp:
                    await checkResponseForError(await resp.text())
                    return await resp.text()

    async def __sendAccountGetRequest(self, uri):
        """Used internally to send GET requests to the Control4 API,
        authenticated with the account bearer token. Returns the entire JSON
        response from the Control4 auth API.

        Parameters:
            `uri` - Full URI to send GET request to.
        """
        try:
            headers = {"Authorization": "Bearer {}".format(self.account_bearer_token)}
        except AttributeError:
            msg = "The account bearer token is missing - was your username/password correct? "
            _LOGGER.error(msg)
            raise
        if self.session is None:
            async with aiohttp.ClientSession() as session:
                with async_timeout.timeout(10):
                    async with session.get(uri, headers=headers) as resp:
                        await checkResponseForError(await resp.text())
                        return await resp.text()
        else:
            with async_timeout.timeout(10):
                async with self.session.get(uri, headers=headers) as resp:
                    await checkResponseForError(await resp.text())
                    return await resp.text()

    async def __sendControllerAuthRequest(self, controller_common_name):
        """Used internally to retrieve an director bearer token. Returns the
        entire JSON response from the Control4 auth API.

        Parameters:
            `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details.
        """
        try:
            headers = {"Authorization": "Bearer {}".format(self.account_bearer_token)}
        except AttributeError:
            msg = "The account bearer token is missing - was your username/password correct? "
            _LOGGER.error(msg)
            raise
        dataDictionary = {
            "serviceInfo": {
                "commonName": controller_common_name,
                "services": "director",
            }
        }
        if self.session is None:
            async with aiohttp.ClientSession() as session:
                with async_timeout.timeout(10):
                    async with session.post(
                        CONTROLLER_AUTHORIZATION_ENDPOINT,
                        headers=headers,
                        json=dataDictionary,
                    ) as resp:
                        await checkResponseForError(await resp.text())
                        return await resp.text()
        else:
            with async_timeout.timeout(10):
                async with self.session.post(
                    CONTROLLER_AUTHORIZATION_ENDPOINT,
                    headers=headers,
                    json=dataDictionary,
                ) as resp:
                    await checkResponseForError(await resp.text())
                    return await resp.text()

    async def getAccountBearerToken(self):
        """Gets an account bearer token for making Control4 online API requests."""
        data = await self.__sendAccountAuthRequest()
        jsonDictionary = json.loads(data)
        try:
            self.account_bearer_token = jsonDictionary["authToken"]["token"]
            return self.account_bearer_token
        except KeyError:
            msg = "Did not recieve an account bearer token. Is your username/password correct? "
            _LOGGER.error(msg + data)
            raise

    async def getAccountControllers(self):
        """Returns a dictionary of the information for all controllers registered to an account.

        Returns:
            ```
            {
                "controllerCommonName": "control4_MODEL_MACADDRESS",
                "href": "https://apis.control4.com/account/v3/rest/accounts/000000",
                "name": "Name"
            }
            ```
        """
        data = await self.__sendAccountGetRequest(GET_CONTROLLERS_ENDPOINT)
        jsonDictionary = json.loads(data)
        return jsonDictionary["account"]

    async def getControllerInfo(self, controller_href):
        """Returns a dictionary of the information of a specific controller.

        Parameters:
            `controller_href` - The API `href` of the controller (get this from the output of `getAccountControllers()`)

        Returns:
            ```
            {
                'allowsPatching': True,
                'allowsSupport': False,
                'blockNotifications': False,
                'controllerCommonName': 'control4_MODEL_MACADDRESS',
                'controller': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/controller'
                },
                'created': '2017-08-26T18:33:31Z',
                'dealer': {
                    'href': 'https://apis.control4.com/account/v3/rest/dealers/12345'
                },
                'enabled': True,
                'hasLoggedIn': True,
                'href': 'https://apis.control4.com/account/v3/rest/accounts/000000',
                'id': 000000,
                'lastCheckIn': '2020-06-13T21:52:34Z',
                'licenses': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/licenses'
                },
                'modified': '2020-06-13T21:52:34Z',
                'name': 'Name',
                'provisionDate': '2017-08-26T18:35:11Z',
                'storage': {
                    'href': 'https://apis.control4.com/storage/v1/rest/accounts/000000'
                },
                'type': 'Consumer',
                'users': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/users'
                }
            }
            ```
        """
        data = await self.__sendAccountGetRequest(controller_href)
        jsonDictionary = json.loads(data)
        return jsonDictionary

    async def getControllerOSVersion(self, controller_href):
        """Returns the OS version of a controller as a string.

        Parameters:
            `controller_href` - The API `href` of the controller (get this from the output of `getAccountControllers()`)
        """
        data = await self.__sendAccountGetRequest(controller_href + "/controller")
        jsonDictionary = json.loads(data)
        return jsonDictionary["osVersion"]

    async def getDirectorBearerToken(self, controller_common_name):
        """Returns a dictionary with a director bearer token for making Control4 Director API requests, and its time valid in seconds (usually 86400 seconds)

        Parameters:
            `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details.
        """
        data = await self.__sendControllerAuthRequest(controller_common_name)
        jsonDictionary = json.loads(data)
        return {
            "token": jsonDictionary["authToken"]["token"],
            "validSeconds": jsonDictionary["authToken"]["validSeconds"],
        }

Methods

async def getAccountBearerToken(self)

Gets an account bearer token for making Control4 online API requests.

async def getAccountControllers(self)

Returns a dictionary of the information for all controllers registered to an account.

Returns

{
    "controllerCommonName": "control4_MODEL_MACADDRESS",
    "href": "https://apis.control4.com/account/v3/rest/accounts/000000",
    "name": "Name"
}
async def getControllerInfo(self, controller_href)

Returns a dictionary of the information of a specific controller.

Parameters

controller_href - The API href of the controller (get this from the output of getAccountControllers())

Returns

{
    'allowsPatching': True,
    'allowsSupport': False,
    'blockNotifications': False,
    'controllerCommonName': 'control4_MODEL_MACADDRESS',
    'controller': {
        'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/controller'
    },
    'created': '2017-08-26T18:33:31Z',
    'dealer': {
        'href': 'https://apis.control4.com/account/v3/rest/dealers/12345'
    },
    'enabled': True,
    'hasLoggedIn': True,
    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000',
    'id': 000000,
    'lastCheckIn': '2020-06-13T21:52:34Z',
    'licenses': {
        'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/licenses'
    },
    'modified': '2020-06-13T21:52:34Z',
    'name': 'Name',
    'provisionDate': '2017-08-26T18:35:11Z',
    'storage': {
        'href': 'https://apis.control4.com/storage/v1/rest/accounts/000000'
    },
    'type': 'Consumer',
    'users': {
        'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/users'
    }
}
async def getControllerOSVersion(self, controller_href)

Returns the OS version of a controller as a string.

Parameters

controller_href - The API href of the controller (get this from the output of getAccountControllers())

async def getDirectorBearerToken(self, controller_common_name)

Returns a dictionary with a director bearer token for making Control4 Director API requests, and its time valid in seconds (usually 86400 seconds)

Parameters

controller_common_name: Common name of the controller. See getAccountControllers() for details.

pyControl4-1.2.1/docs/alarm.html000066400000000000000000000615111466515773400165300ustar00rootroot00000000000000 pyControl4.alarm API documentation

Module pyControl4.alarm

Controls Control4 security panel and contact sensor (door, window, motion) devices.

Classes

class C4ContactSensor (C4Director, item_id)

Creates a Control4 Contact Sensor object.

Parameters

C4Director - A C4Director object that corresponds to the Control4 Director that the security panel is connected to.

item_id - The Control4 item ID of the contact sensor.

Expand source code
class C4ContactSensor:
    def __init__(self, C4Director, item_id):
        """Creates a Control4 Contact Sensor object.

        Parameters:
            `C4Director` - A `pyControl4.director.C4Director` object that corresponds to the Control4 Director that the security panel is connected to.

            `item_id` - The Control4 item ID of the contact sensor.
        """
        self.director = C4Director
        self.item_id = item_id

    async def getContactState(self):
        """Returns `True` if contact is triggered (door/window is closed, motion is detected), otherwise returns `False`."""
        contact_state = await self.director.getItemVariableValue(
            self.item_id, "ContactState"
        )
        return bool(contact_state)

Methods

async def getContactState(self)

Returns True if contact is triggered (door/window is closed, motion is detected), otherwise returns False.

class C4SecurityPanel (C4Director, item_id)

Creates a Control4 object.

Parameters

C4Director - A C4Director object that corresponds to the Control4 Director that the device is connected to.

item_id - The Control4 item ID.

Expand source code
class C4SecurityPanel(C4Entity):
    async def getArmState(self):
        """Returns the arm state of the security panel as "DISARMED", "ARMED_HOME", or "ARMED_AWAY"."""
        disarmed = await self.director.getItemVariableValue(
            self.item_id, "DISARMED_STATE"
        )
        armed_home = await self.director.getItemVariableValue(
            self.item_id, "HOME_STATE"
        )
        armed_away = await self.director.getItemVariableValue(
            self.item_id, "AWAY_STATE"
        )
        if disarmed == 1:
            return "DISARMED"
        elif armed_home == 1:
            return "ARMED_HOME"
        elif armed_away == 1:
            return "ARMED_AWAY"

    async def getAlarmState(self):
        """Returns `True` if alarm is triggered, otherwise returns `False`."""
        alarm_state = await self.director.getItemVariableValue(
            self.item_id, "ALARM_STATE"
        )
        return bool(alarm_state)

    async def getDisplayText(self):
        """Returns the display text of the security panel."""
        display_text = await self.director.getItemVariableValue(
            self.item_id, "DISPLAY_TEXT"
        )
        return display_text

    async def getTroubleText(self):
        """Returns the trouble display text of the security panel."""
        trouble_text = await self.director.getItemVariableValue(
            self.item_id, "TROUBLE_TEXT"
        )
        return trouble_text

    async def getPartitionState(self):
        """Returns the partition state of the security panel.

        Possible values include "DISARMED_NOT_READY", "DISARMED_READY", "ARMED_HOME", "ARMED_AWAY", "EXIT_DELAY", "ENTRY_DELAY"
        """
        partition_state = await self.director.getItemVariableValue(
            self.item_id, "PARTITION_STATE"
        )
        return partition_state

    async def getDelayTimeTotal(self):
        """Returns the total exit delay time. Returns 0 if an exit delay is not currently running."""
        delay_time_total = await self.director.getItemVariableValue(
            self.item_id, "DELAY_TIME_TOTAL"
        )
        return delay_time_total

    async def getDelayTimeRemaining(self):
        """Returns the remaining exit delay time. Returns 0 if an exit delay is not currently running."""
        delay_time_remaining = await self.director.getItemVariableValue(
            self.item_id, "DELAY_TIME_REMAINING"
        )
        return delay_time_remaining

    async def getOpenZoneCount(self):
        """Returns the number of open/unsecured zones."""
        open_zone_count = await self.director.getItemVariableValue(
            self.item_id, "OPEN_ZONE_COUNT"
        )
        return open_zone_count

    async def getAlarmType(self):
        """Returns details about the current alarm type."""
        alarm_type = await self.director.getItemVariableValue(
            self.item_id, "ALARM_TYPE"
        )
        return alarm_type

    async def getArmedType(self):
        """Returns details about the current arm type."""
        armed_type = await self.director.getItemVariableValue(
            self.item_id, "ARMED_TYPE"
        )
        return armed_type

    async def getLastEmergency(self):
        """Returns details about the last emergency trigger."""
        last_emergency = await self.director.getItemVariableValue(
            self.item_id, "LAST_EMERGENCY"
        )
        return last_emergency

    async def getLastArmFailure(self):
        """Returns details about the last arm failure."""
        last_arm_failed = await self.director.getItemVariableValue(
            self.item_id, "LAST_ARM_FAILED"
        )
        return last_arm_failed

    async def setArm(self, usercode, mode: str):
        """Arms the security panel with the specified mode.

        Parameters:
            `usercode` - PIN/code for arming the system.

            `mode` - Arm mode to use. This depends on what is supported by the security panel itself.
        """
        usercode = str(usercode)
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "PARTITION_ARM",
            {"ArmType": mode, "UserCode": usercode},
        )

    async def setDisarm(self, usercode):
        """Disarms the security panel.

        Parameters:
            `usercode` - PIN/code for disarming the system.
        """
        usercode = str(usercode)
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "PARTITION_DISARM",
            {"UserCode": usercode},
        )

    async def getEmergencyTypes(self):
        """Returns the available emergency types as a list.

        Possible types are "Fire", "Medical", "Panic", and "Police".
        """
        types_list = []

        data = await self.director.getItemInfo(self.item_id)
        jsonDictionary = json.loads(data)

        if jsonDictionary[0]["capabilities"]["has_fire"]:
            types_list.append("Fire")
        if jsonDictionary[0]["capabilities"]["has_medical"]:
            types_list.append("Medical")
        if jsonDictionary[0]["capabilities"]["has_panic"]:
            types_list.append("Panic")
        if jsonDictionary[0]["capabilities"]["has_police"]:
            types_list.append("Police")

        return types_list

    async def triggerEmergency(self, usercode, type):
        """Triggers an emergency of the specified type.

        Parameters:
            `usercode` - PIN/code for disarming the system.

            `type` - Type of emergency: "Fire", "Medical", "Panic", or "Police"
        """
        usercode = str(usercode)
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "PARTITION_DISARM",
            {"UserCode": usercode},
        )

    async def sendKeyPress(self, key):
        """Sends a single keypress to the security panel's virtual keypad (if supported).

        Parameters:
            `key` - Keypress to send. Only one key at a time.
        """
        key = str(key)
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "KEY_PRESS",
            {"KeyName": key},
        )

Ancestors

Methods

async def getAlarmState(self)

Returns True if alarm is triggered, otherwise returns False.

async def getAlarmType(self)

Returns details about the current alarm type.

async def getArmState(self)

Returns the arm state of the security panel as "DISARMED", "ARMED_HOME", or "ARMED_AWAY".

async def getArmedType(self)

Returns details about the current arm type.

async def getDelayTimeRemaining(self)

Returns the remaining exit delay time. Returns 0 if an exit delay is not currently running.

async def getDelayTimeTotal(self)

Returns the total exit delay time. Returns 0 if an exit delay is not currently running.

async def getDisplayText(self)

Returns the display text of the security panel.

async def getEmergencyTypes(self)

Returns the available emergency types as a list.

Possible types are "Fire", "Medical", "Panic", and "Police".

async def getLastArmFailure(self)

Returns details about the last arm failure.

async def getLastEmergency(self)

Returns details about the last emergency trigger.

async def getOpenZoneCount(self)

Returns the number of open/unsecured zones.

async def getPartitionState(self)

Returns the partition state of the security panel.

Possible values include "DISARMED_NOT_READY", "DISARMED_READY", "ARMED_HOME", "ARMED_AWAY", "EXIT_DELAY", "ENTRY_DELAY"

async def getTroubleText(self)

Returns the trouble display text of the security panel.

async def sendKeyPress(self, key)

Sends a single keypress to the security panel's virtual keypad (if supported).

Parameters

key - Keypress to send. Only one key at a time.

async def setArm(self, usercode, mode: str)

Arms the security panel with the specified mode.

Parameters

usercode - PIN/code for arming the system.

mode - Arm mode to use. This depends on what is supported by the security panel itself.

async def setDisarm(self, usercode)

Disarms the security panel.

Parameters

usercode - PIN/code for disarming the system.

async def triggerEmergency(self, usercode, type)

Triggers an emergency of the specified type.

Parameters

usercode - PIN/code for disarming the system.

type - Type of emergency: "Fire", "Medical", "Panic", or "Police"

pyControl4-1.2.1/docs/auth.html000066400000000000000000000756531466515773400164110ustar00rootroot00000000000000 pyControl4.auth API documentation

Module pyControl4.auth

Authenticates with the Control4 API, retrieves account and registered controller info, and retrieves a bearer token for connecting to a Control4 Director.

Expand source code
"""Authenticates with the Control4 API, retrieves account and registered controller info, and retrieves a bearer token for connecting to a Control4 Director.
"""
import aiohttp
import asyncio
import async_timeout
import json
import logging

AUTHENTICATION_ENDPOINT = "https://apis.control4.com/authentication/v1/rest"
CONTROLLER_AUTHORIZATION_ENDPOINT = (
    "https://apis.control4.com/authentication/v1/rest/authorization"
)
GET_CONTROLLERS_ENDPOINT = "https://apis.control4.com/account/v3/rest/accounts"
APPLICATION_KEY = "78f6791373d61bea49fdb9fb8897f1f3af193f11"

_LOGGER = logging.getLogger(__name__)


class C4Auth:
    def __init__(self, username, password):
        self.username = username
        self.password = password
        asyncio.run(self.getAccountBearerToken())

    async def __sendAccountAuthRequest(self):
        """Used internally to retrieve an account bearer token. Returns the entire JSON response from the Control4 auth API.

        Parameters:
            `username` - Control4 account username/email.

            `password` - Control4 account password.
        """
        dataDictionary = {
            "clientInfo": {
                "device": {
                    "deviceName": "pyControl4",
                    "deviceUUID": "0000000000000000",
                    "make": "pyControl4",
                    "model": "pyControl4",
                    "os": "Android",
                    "osVersion": "10",
                },
                "userInfo": {
                    "applicationKey": APPLICATION_KEY,
                    "password": self.password,
                    "userName": self.username,
                },
            }
        }
        async with aiohttp.ClientSession() as session:
            with async_timeout.timeout(10):
                async with session.post(
                    AUTHENTICATION_ENDPOINT, json=dataDictionary
                ) as resp:
                    return await resp.text()

    async def __sendAccountGetRequest(self, uri):
        """Used internally to send GET requests to the Control4 API, authenticated with the account bearer token. Returns the entire JSON response from the Control4 auth API.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

            `uri` - Full URI to send GET request to.
        """
        try:
            headers = {"Authorization": "Bearer {}".format(self.account_bearer_token)}
        except AttributeError:
            msg = "The account bearer token is missing - was your username/password correct? "
            _LOGGER.error(msg)
            raise
        async with aiohttp.ClientSession() as session:
            with async_timeout.timeout(10):
                async with session.get(uri, headers=headers) as resp:
                    return await resp.text()

    async def __sendControllerAuthRequest(self, controller_common_name):
        """Used internally to retrieve an director bearer token. Returns the entire JSON response from the Control4 auth API.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

            `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details.
        """
        try:
            headers = {"Authorization": "Bearer {}".format(self.account_bearer_token)}
        except AttributeError:
            msg = "The account bearer token is missing - was your username/password correct? "
            _LOGGER.error(msg)
            raise
        dataDictionary = {
            "serviceInfo": {
                "commonName": controller_common_name,
                "services": "director",
            }
        }
        async with aiohttp.ClientSession() as session:
            with async_timeout.timeout(10):
                async with session.post(
                    CONTROLLER_AUTHORIZATION_ENDPOINT,
                    headers=headers,
                    json=dataDictionary,
                ) as resp:
                    return await resp.text()

    async def getAccountBearerToken(self):
        """Returns an account bearer token for making Control4 online API requests.

        Parameters:
            `username` - Control4 account username/email.

            `password` - Control4 account password.
        """
        data = await self.__sendAccountAuthRequest()
        jsonDictionary = json.loads(data)
        try:
            self.account_bearer_token = jsonDictionary["authToken"]["token"]
        except KeyError:
            msg = "Did not recieve an account bearer token. Is your username/password correct? "
            _LOGGER.error(msg + data)
            raise

    async def getAccountControllers(self):
        """Returns a dictionary of the information for all controllers registered to an account.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

        Returns:
            ```
            {    
                "controllerCommonName": "control4_MODEL_MACADDRESS",
                "href": "https://apis.control4.com/account/v3/rest/accounts/000000",
                "name": "Name"
            }
            ```
        """
        data = await self.__sendAccountGetRequest(GET_CONTROLLERS_ENDPOINT)
        jsonDictionary = json.loads(data)
        return jsonDictionary["account"]

    async def getControllerInfo(self, controller_href):
        """Returns a dictionary of the information of a specific controller.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

            `controller_href` - The API `href` of the controller (get this from the output of `getAccountControllers()`)

        Returns:
            ```
            {
                'allowsPatching': True,
                'allowsSupport': False,
                'blockNotifications': False,
                'controllerCommonName': 'control4_MODEL_MACADDRESS',
                'controller': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/controller'
                },
                'created': '2017-08-26T18:33:31Z',
                'dealer': {
                    'href': 'https://apis.control4.com/account/v3/rest/dealers/12345'
                },
                'enabled': True,
                'hasLoggedIn': True,
                'href': 'https://apis.control4.com/account/v3/rest/accounts/000000',
                'id': 000000,
                'lastCheckIn': '2020-06-13T21:52:34Z',
                'licenses': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/licenses'
                },
                'modified': '2020-06-13T21:52:34Z',
                'name': 'Name',
                'provisionDate': '2017-08-26T18:35:11Z',
                'storage': {
                    'href': 'https://apis.control4.com/storage/v1/rest/accounts/000000'
                },
                'type': 'Consumer',
                'users': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/users'
                }
            }
            ```
        """
        data = await self.__sendAccountGetRequest(controller_href)
        jsonDictionary = json.loads(data)
        return jsonDictionary

    async def getDirectorBearerToken(self, controller_common_name):
        """Returns a director bearer token for making Control4 Director API requests.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

            `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details.
        """
        data = await self.__sendControllerAuthRequest(controller_common_name)
        jsonDictionary = json.loads(data)
        return jsonDictionary["authToken"]["token"]

Classes

class C4Auth (username, password)
Expand source code
class C4Auth:
    def __init__(self, username, password):
        self.username = username
        self.password = password
        asyncio.run(self.getAccountBearerToken())

    async def __sendAccountAuthRequest(self):
        """Used internally to retrieve an account bearer token. Returns the entire JSON response from the Control4 auth API.

        Parameters:
            `username` - Control4 account username/email.

            `password` - Control4 account password.
        """
        dataDictionary = {
            "clientInfo": {
                "device": {
                    "deviceName": "pyControl4",
                    "deviceUUID": "0000000000000000",
                    "make": "pyControl4",
                    "model": "pyControl4",
                    "os": "Android",
                    "osVersion": "10",
                },
                "userInfo": {
                    "applicationKey": APPLICATION_KEY,
                    "password": self.password,
                    "userName": self.username,
                },
            }
        }
        async with aiohttp.ClientSession() as session:
            with async_timeout.timeout(10):
                async with session.post(
                    AUTHENTICATION_ENDPOINT, json=dataDictionary
                ) as resp:
                    return await resp.text()

    async def __sendAccountGetRequest(self, uri):
        """Used internally to send GET requests to the Control4 API, authenticated with the account bearer token. Returns the entire JSON response from the Control4 auth API.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

            `uri` - Full URI to send GET request to.
        """
        try:
            headers = {"Authorization": "Bearer {}".format(self.account_bearer_token)}
        except AttributeError:
            msg = "The account bearer token is missing - was your username/password correct? "
            _LOGGER.error(msg)
            raise
        async with aiohttp.ClientSession() as session:
            with async_timeout.timeout(10):
                async with session.get(uri, headers=headers) as resp:
                    return await resp.text()

    async def __sendControllerAuthRequest(self, controller_common_name):
        """Used internally to retrieve an director bearer token. Returns the entire JSON response from the Control4 auth API.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

            `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details.
        """
        try:
            headers = {"Authorization": "Bearer {}".format(self.account_bearer_token)}
        except AttributeError:
            msg = "The account bearer token is missing - was your username/password correct? "
            _LOGGER.error(msg)
            raise
        dataDictionary = {
            "serviceInfo": {
                "commonName": controller_common_name,
                "services": "director",
            }
        }
        async with aiohttp.ClientSession() as session:
            with async_timeout.timeout(10):
                async with session.post(
                    CONTROLLER_AUTHORIZATION_ENDPOINT,
                    headers=headers,
                    json=dataDictionary,
                ) as resp:
                    return await resp.text()

    async def getAccountBearerToken(self):
        """Returns an account bearer token for making Control4 online API requests.

        Parameters:
            `username` - Control4 account username/email.

            `password` - Control4 account password.
        """
        data = await self.__sendAccountAuthRequest()
        jsonDictionary = json.loads(data)
        try:
            self.account_bearer_token = jsonDictionary["authToken"]["token"]
        except KeyError:
            msg = "Did not recieve an account bearer token. Is your username/password correct? "
            _LOGGER.error(msg + data)
            raise

    async def getAccountControllers(self):
        """Returns a dictionary of the information for all controllers registered to an account.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

        Returns:
            ```
            {    
                "controllerCommonName": "control4_MODEL_MACADDRESS",
                "href": "https://apis.control4.com/account/v3/rest/accounts/000000",
                "name": "Name"
            }
            ```
        """
        data = await self.__sendAccountGetRequest(GET_CONTROLLERS_ENDPOINT)
        jsonDictionary = json.loads(data)
        return jsonDictionary["account"]

    async def getControllerInfo(self, controller_href):
        """Returns a dictionary of the information of a specific controller.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

            `controller_href` - The API `href` of the controller (get this from the output of `getAccountControllers()`)

        Returns:
            ```
            {
                'allowsPatching': True,
                'allowsSupport': False,
                'blockNotifications': False,
                'controllerCommonName': 'control4_MODEL_MACADDRESS',
                'controller': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/controller'
                },
                'created': '2017-08-26T18:33:31Z',
                'dealer': {
                    'href': 'https://apis.control4.com/account/v3/rest/dealers/12345'
                },
                'enabled': True,
                'hasLoggedIn': True,
                'href': 'https://apis.control4.com/account/v3/rest/accounts/000000',
                'id': 000000,
                'lastCheckIn': '2020-06-13T21:52:34Z',
                'licenses': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/licenses'
                },
                'modified': '2020-06-13T21:52:34Z',
                'name': 'Name',
                'provisionDate': '2017-08-26T18:35:11Z',
                'storage': {
                    'href': 'https://apis.control4.com/storage/v1/rest/accounts/000000'
                },
                'type': 'Consumer',
                'users': {
                    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/users'
                }
            }
            ```
        """
        data = await self.__sendAccountGetRequest(controller_href)
        jsonDictionary = json.loads(data)
        return jsonDictionary

    async def getDirectorBearerToken(self, controller_common_name):
        """Returns a director bearer token for making Control4 Director API requests.

        Parameters:
            `account_bearer_token` - Control4 account bearer token.

            `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details.
        """
        data = await self.__sendControllerAuthRequest(controller_common_name)
        jsonDictionary = json.loads(data)
        return jsonDictionary["authToken"]["token"]

Methods

async def getAccountBearerToken(self)

Returns an account bearer token for making Control4 online API requests.

Parameters

username - Control4 account username/email.

password - Control4 account password.

Expand source code
async def getAccountBearerToken(self):
    """Returns an account bearer token for making Control4 online API requests.

    Parameters:
        `username` - Control4 account username/email.

        `password` - Control4 account password.
    """
    data = await self.__sendAccountAuthRequest()
    jsonDictionary = json.loads(data)
    try:
        self.account_bearer_token = jsonDictionary["authToken"]["token"]
    except KeyError:
        msg = "Did not recieve an account bearer token. Is your username/password correct? "
        _LOGGER.error(msg + data)
        raise
async def getAccountControllers(self)

Returns a dictionary of the information for all controllers registered to an account.

Parameters

account_bearer_token - Control4 account bearer token.

Returns

{    
    "controllerCommonName": "control4_MODEL_MACADDRESS",
    "href": "https://apis.control4.com/account/v3/rest/accounts/000000",
    "name": "Name"
}
Expand source code
async def getAccountControllers(self):
    """Returns a dictionary of the information for all controllers registered to an account.

    Parameters:
        `account_bearer_token` - Control4 account bearer token.

    Returns:
        ```
        {    
            "controllerCommonName": "control4_MODEL_MACADDRESS",
            "href": "https://apis.control4.com/account/v3/rest/accounts/000000",
            "name": "Name"
        }
        ```
    """
    data = await self.__sendAccountGetRequest(GET_CONTROLLERS_ENDPOINT)
    jsonDictionary = json.loads(data)
    return jsonDictionary["account"]
async def getControllerInfo(self, controller_href)

Returns a dictionary of the information of a specific controller.

Parameters

account_bearer_token - Control4 account bearer token.

controller_href - The API href of the controller (get this from the output of getAccountControllers())

Returns

{
    'allowsPatching': True,
    'allowsSupport': False,
    'blockNotifications': False,
    'controllerCommonName': 'control4_MODEL_MACADDRESS',
    'controller': {
        'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/controller'
    },
    'created': '2017-08-26T18:33:31Z',
    'dealer': {
        'href': 'https://apis.control4.com/account/v3/rest/dealers/12345'
    },
    'enabled': True,
    'hasLoggedIn': True,
    'href': 'https://apis.control4.com/account/v3/rest/accounts/000000',
    'id': 000000,
    'lastCheckIn': '2020-06-13T21:52:34Z',
    'licenses': {
        'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/licenses'
    },
    'modified': '2020-06-13T21:52:34Z',
    'name': 'Name',
    'provisionDate': '2017-08-26T18:35:11Z',
    'storage': {
        'href': 'https://apis.control4.com/storage/v1/rest/accounts/000000'
    },
    'type': 'Consumer',
    'users': {
        'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/users'
    }
}
Expand source code
async def getControllerInfo(self, controller_href):
    """Returns a dictionary of the information of a specific controller.

    Parameters:
        `account_bearer_token` - Control4 account bearer token.

        `controller_href` - The API `href` of the controller (get this from the output of `getAccountControllers()`)

    Returns:
        ```
        {
            'allowsPatching': True,
            'allowsSupport': False,
            'blockNotifications': False,
            'controllerCommonName': 'control4_MODEL_MACADDRESS',
            'controller': {
                'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/controller'
            },
            'created': '2017-08-26T18:33:31Z',
            'dealer': {
                'href': 'https://apis.control4.com/account/v3/rest/dealers/12345'
            },
            'enabled': True,
            'hasLoggedIn': True,
            'href': 'https://apis.control4.com/account/v3/rest/accounts/000000',
            'id': 000000,
            'lastCheckIn': '2020-06-13T21:52:34Z',
            'licenses': {
                'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/licenses'
            },
            'modified': '2020-06-13T21:52:34Z',
            'name': 'Name',
            'provisionDate': '2017-08-26T18:35:11Z',
            'storage': {
                'href': 'https://apis.control4.com/storage/v1/rest/accounts/000000'
            },
            'type': 'Consumer',
            'users': {
                'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/users'
            }
        }
        ```
    """
    data = await self.__sendAccountGetRequest(controller_href)
    jsonDictionary = json.loads(data)
    return jsonDictionary
async def getDirectorBearerToken(self, controller_common_name)

Returns a director bearer token for making Control4 Director API requests.

Parameters

account_bearer_token - Control4 account bearer token.

controller_common_name: Common name of the controller. See getAccountControllers() for details.

Expand source code
async def getDirectorBearerToken(self, controller_common_name):
    """Returns a director bearer token for making Control4 Director API requests.

    Parameters:
        `account_bearer_token` - Control4 account bearer token.

        `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details.
    """
    data = await self.__sendControllerAuthRequest(controller_common_name)
    jsonDictionary = json.loads(data)
    return jsonDictionary["authToken"]["token"]
pyControl4-1.2.1/docs/blind.html000066400000000000000000000446001466515773400165240ustar00rootroot00000000000000 pyControl4.blind API documentation

Module pyControl4.blind

Controls Control4 blind devices.

Classes

class C4Blind (C4Director, item_id)

Creates a Control4 object.

Parameters

C4Director - A C4Director object that corresponds to the Control4 Director that the device is connected to.

item_id - The Control4 item ID.

Expand source code
class C4Blind(C4Entity):
    async def getBatteryLevel(self):
        """Returns the battery of a blind. We currently don't know the range or meaning."""
        value = await self.director.getItemVariableValue(self.item_id, "Battery Level")
        return int(value)

    async def getClosing(self):
        """Returns an indication of whether the blind is moving in the closed direction as a boolean
        (True=closing, False=opening). If the blind is stopped, reports the direction it last moved.
        """
        value = await self.director.getItemVariableValue(self.item_id, "Closing")
        return bool(value)

    async def getFullyClosed(self):
        """Returns an indication of whether the blind is fully closed as a boolean
        (True=fully closed, False=at least partially open)."""
        value = await self.director.getItemVariableValue(self.item_id, "Fully Closed")
        return bool(value)

    async def getFullyOpen(self):
        """Returns an indication of whether the blind is fully open as a boolean
        (True=fully open, False=at least partially closed)."""
        value = await self.director.getItemVariableValue(self.item_id, "Fully Open")
        return bool(value)

    async def getLevel(self):
        """Returns the level (current position) of a blind as an int 0-100.
        0 is fully closed and 100 is fully open.
        """
        value = await self.director.getItemVariableValue(self.item_id, "Level")
        return int(value)

    async def getOpen(self):
        """Returns an indication of whether the blind is open as a boolean (True=open, False=closed).
        This is true even if the blind is only partially open."""
        value = await self.director.getItemVariableValue(self.item_id, "Open")
        return bool(value)

    async def getOpening(self):
        """Returns an indication of whether the blind is moving in the open direction as a boolean
        (True=opening, False=closing). If the blind is stopped, reports the direction it last moved.
        """
        value = await self.director.getItemVariableValue(self.item_id, "Opening")
        return bool(value)

    async def getStopped(self):
        """Returns an indication of whether the blind is stopped as a boolean
        (True=stopped, False=moving)."""
        value = await self.director.getItemVariableValue(self.item_id, "Stopped")
        return bool(value)

    async def getTargetLevel(self):
        """Returns the target level (desired position) of a blind as an int 0-100.
         The blind will move if this is different from the current level.
        0 is fully closed and 100 is fully open.
        """
        value = await self.director.getItemVariableValue(self.item_id, "Target Level")
        return int(value)

    async def open(self):
        """Opens the blind completely."""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "SET_LEVEL_TARGET:LEVEL_TARGET_OPEN",
            {},
        )

    async def close(self):
        """Closes the blind completely."""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "SET_LEVEL_TARGET:LEVEL_TARGET_CLOSED",
            {},
        )

    async def setLevelTarget(self, level):
        """Sets the desired level of a blind; it will start moving towards that level.
        Level 0 is fully closed and level 100 is fully open.

        Parameters:
            `level` - (int) 0-100
        """
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "SET_LEVEL_TARGET",
            {"LEVEL_TARGET": level},
        )

    async def stop(self):
        """Stops the blind if it is moving. Shortly after stopping, the target level will be
        set to the level the blind had actually reached when it stopped."""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "STOP",
            {},
        )

    async def toggle(self):
        """Toggles the blind between open and closed. Has no effect if the blind is partially open."""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "TOGGLE",
            {},
        )

Ancestors

Methods

async def close(self)

Closes the blind completely.

async def getBatteryLevel(self)

Returns the battery of a blind. We currently don't know the range or meaning.

async def getClosing(self)

Returns an indication of whether the blind is moving in the closed direction as a boolean (True=closing, False=opening). If the blind is stopped, reports the direction it last moved.

async def getFullyClosed(self)

Returns an indication of whether the blind is fully closed as a boolean (True=fully closed, False=at least partially open).

async def getFullyOpen(self)

Returns an indication of whether the blind is fully open as a boolean (True=fully open, False=at least partially closed).

async def getLevel(self)

Returns the level (current position) of a blind as an int 0-100. 0 is fully closed and 100 is fully open.

async def getOpen(self)

Returns an indication of whether the blind is open as a boolean (True=open, False=closed). This is true even if the blind is only partially open.

async def getOpening(self)

Returns an indication of whether the blind is moving in the open direction as a boolean (True=opening, False=closing). If the blind is stopped, reports the direction it last moved.

async def getStopped(self)

Returns an indication of whether the blind is stopped as a boolean (True=stopped, False=moving).

async def getTargetLevel(self)

Returns the target level (desired position) of a blind as an int 0-100. The blind will move if this is different from the current level. 0 is fully closed and 100 is fully open.

async def open(self)

Opens the blind completely.

async def setLevelTarget(self, level)

Sets the desired level of a blind; it will start moving towards that level. Level 0 is fully closed and level 100 is fully open.

Parameters

level - (int) 0-100

async def stop(self)

Stops the blind if it is moving. Shortly after stopping, the target level will be set to the level the blind had actually reached when it stopped.

async def toggle(self)

Toggles the blind between open and closed. Has no effect if the blind is partially open.

pyControl4-1.2.1/docs/director.html000066400000000000000000000666641466515773400172650ustar00rootroot00000000000000 pyControl4.director API documentation

Module pyControl4.director

Handles communication with a Control4 Director, and provides functions for getting details about items on the Director.

Classes

class C4Director (ip, director_bearer_token, session_no_verify_ssl: aiohttp.client.ClientSession = None)

Creates a Control4 Director object.

Parameters

ip - The IP address of the Control4 Director/Controller.

director_bearer_token - The bearer token used to authenticate with the Director. See C4Account.getDirectorBearerToken() for how to get this.

session - (Optional) Allows the use of an aiohttp.ClientSession object for all network requests. This session will not be closed by the library. If not provided, the library will open and close its own ClientSessions as needed.

Expand source code
class C4Director:
    def __init__(
        self,
        ip,
        director_bearer_token,
        session_no_verify_ssl: aiohttp.ClientSession = None,
    ):
        """Creates a Control4 Director object.

        Parameters:
            `ip` - The IP address of the Control4 Director/Controller.

            `director_bearer_token` - The bearer token used to authenticate
                                      with the Director.
                See `pyControl4.account.C4Account.getDirectorBearerToken`
                for how to get this.

            `session` - (Optional) Allows the use of an
                        `aiohttp.ClientSession` object
                        for all network requests. This
                        session will not be closed by the library.
                        If not provided, the library will open and
                        close its own `ClientSession`s as needed.
        """
        self.base_url = "https://{}".format(ip)
        self.headers = {"Authorization": "Bearer {}".format(director_bearer_token)}
        self.director_bearer_token = director_bearer_token
        self.session = session_no_verify_ssl

    async def sendGetRequest(self, uri):
        """Sends a GET request to the specified API URI.
        Returns the Director's JSON response as a string.

        Parameters:
            `uri` - The API URI to send the request to. Do not include the IP
                    address of the Director.
        """
        if self.session is None:
            async with aiohttp.ClientSession(
                connector=aiohttp.TCPConnector(verify_ssl=False)
            ) as session:
                with async_timeout.timeout(10):
                    async with session.get(
                        self.base_url + uri, headers=self.headers
                    ) as resp:
                        await checkResponseForError(await resp.text())
                        return await resp.text()
        else:
            with async_timeout.timeout(10):
                async with self.session.get(
                    self.base_url + uri, headers=self.headers
                ) as resp:
                    await checkResponseForError(await resp.text())
                    return await resp.text()

    async def sendPostRequest(self, uri, command, params, async_variable=True):
        """Sends a POST request to the specified API URI. Used to send commands
           to the Director.
        Returns the Director's JSON response as a string.

        Parameters:
            `uri` - The API URI to send the request to. Do not include the IP
                    address of the Director.

            `command` - The Control4 command to send.

            `params` - The parameters of the command, provided as a dictionary.
        """
        dataDictionary = {
            "async": async_variable,
            "command": command,
            "tParams": params,
        }
        if self.session is None:
            async with aiohttp.ClientSession(
                connector=aiohttp.TCPConnector(verify_ssl=False)
            ) as session:
                with async_timeout.timeout(10):
                    async with session.post(
                        self.base_url + uri, headers=self.headers, json=dataDictionary
                    ) as resp:
                        await checkResponseForError(await resp.text())
                        return await resp.text()
        else:
            with async_timeout.timeout(10):
                async with self.session.post(
                    self.base_url + uri, headers=self.headers, json=dataDictionary
                ) as resp:
                    await checkResponseForError(await resp.text())
                    return await resp.text()

    async def getAllItemsByCategory(self, category):
        """Returns a JSON list of items related to a particular category.

        Parameters:
            `category` - Control4 Category Name: controllers, comfort, lights,
                         cameras, sensors, audio_video,
                         motorization, thermostats, motors,
                         control4_remote_hub,
                         outlet_wireless_dimmer, voice-scene
        """
        return_list = await self.sendGetRequest(
            "/api/v1/categories/{}".format(category)
        )
        return return_list

    async def getAllItemInfo(self):
        """Returns a JSON list of all the items on the Director."""
        return await self.sendGetRequest("/api/v1/items")

    async def getItemInfo(self, item_id):
        """Returns a JSON list of the details of the specified item.

        Parameters:
            `item_id` - The Control4 item ID.
        """
        return await self.sendGetRequest("/api/v1/items/{}".format(item_id))

    async def getItemSetup(self, item_id):
        """Returns a JSON list of the setup info of the specified item.

        Parameters:
            `item_id` - The Control4 item ID.
        """
        return await self.sendPostRequest(
            "/api/v1/items/{}/commands".format(item_id), "GET_SETUP", {}, False
        )

    async def getItemVariables(self, item_id):
        """Returns a JSON list of the variables available for the specified item.

        Parameters:
            `item_id` - The Control4 item ID.
        """
        return await self.sendGetRequest("/api/v1/items/{}/variables".format(item_id))

    async def getItemVariableValue(self, item_id, var_name):
        """Returns the value of the specified variable for the
        specified item as a string.

        Parameters:
            `item_id` - The Control4 item ID.

            `var_name` - The Control4 variable name or names.
        """

        if isinstance(var_name, (tuple, list, set)):
            var_name = ",".join(var_name)

        data = await self.sendGetRequest(
            "/api/v1/items/{}/variables?varnames={}".format(item_id, var_name)
        )
        if data == "[]":
            raise ValueError(
                "Empty response recieved from Director! The variable {} \
                    doesn't seem to exist for item {}.".format(
                    var_name, item_id
                )
            )
        jsonDictionary = json.loads(data)
        return jsonDictionary[0]["value"]

    async def getAllItemVariableValue(self, var_name):
        """Returns a dictionary with the values of the specified variable
        for all items that have it.

        Parameters:
            `var_name` - The Control4 variable name or names.
        """
        if isinstance(var_name, (tuple, list, set)):
            var_name = ",".join(var_name)

        data = await self.sendGetRequest(
            "/api/v1/items/variables?varnames={}".format(var_name)
        )
        if data == "[]":
            raise ValueError(
                "Empty response recieved from Director! The variable {} \
                    doesn't seem to exist for any items.".format(
                    var_name
                )
            )
        jsonDictionary = json.loads(data)
        return jsonDictionary

    async def getItemCommands(self, item_id):
        """Returns a JSON list of the commands available for the specified item.

        Parameters:
            `item_id` - The Control4 item ID.
        """
        return await self.sendGetRequest("/api/v1/items/{}/commands".format(item_id))

    async def getItemNetwork(self, item_id):
        """Returns a JSON list of the network information for the specified item.

        Parameters:
            `item_id` - The Control4 item ID.
        """
        return await self.sendGetRequest("/api/v1/items/{}/network".format(item_id))

    async def getItemBindings(self, item_id):
        """Returns a JSON list of the bindings information for the specified item.

        Parameters:
            `item_id` - The Control4 item ID.
        """
        return await self.sendGetRequest("/api/v1/items/{}/bindings".format(item_id))

    async def getUiConfiguration(self):
        """Returns a dictionary of the JSON Control4 App UI Configuration enumerating rooms and capabilities

        Returns:

            {
             "experiences": [
                {
                 "type": "watch",
                 "sources": {
                    "source": [
                     {
                      "id": 59,
                      "type": "HDMI"
                     },
                     {
                      "id": 946,
                      "type": "HDMI"
                     },
                     {
                      "id": 950,
                      "type": "HDMI"
                     },
                     {
                      "id": 33,
                      "type": "VIDEO_SELECTION"
                     }
                    ]
                },
                 "active": false,
                 "room_id": 9,
                 "username": "primaryuser"
                },
                {
                 "type": "listen",
                 "sources": {
                    "source": [
                    {
                     "id": 298,
                     "type": "DIGITAL_AUDIO_SERVER",
                     "name": "My Music"
                    },
                    {
                     "id": 302,
                     "type": "AUDIO_SELECTION",
                     "name": "Stations"
                    },
                    {
                     "id": 306,
                     "type": "DIGITAL_AUDIO_SERVER",
                     "name": "ShairBridge"
                    },
                    {
                     "id": 937,
                     "type": "DIGITAL_AUDIO_SERVER",
                     "name": "Spotify Connect"
                    },
                    {
                     "id": 100002,
                     "type": "DIGITAL_AUDIO_CLIENT",
                     "name": "Digital Media"
                    }
                   ]
                },
                 "active": false,
                 "room_id": 9,
                 "username": "primaryuser"
                },
                {
                 "type": "cameras",
                 "sources": {
                    "source": [
                    {
                     "id": 877,
                     "type": "Camera"
                    },
                    ...
                }
                ...
            }
        """

        return await self.sendGetRequest("/api/v1/agents/ui_configuration")

Methods

async def getAllItemInfo(self)

Returns a JSON list of all the items on the Director.

async def getAllItemVariableValue(self, var_name)

Returns a dictionary with the values of the specified variable for all items that have it.

Parameters

var_name - The Control4 variable name or names.

async def getAllItemsByCategory(self, category)

Returns a JSON list of items related to a particular category.

Parameters

category - Control4 Category Name: controllers, comfort, lights, cameras, sensors, audio_video, motorization, thermostats, motors, control4_remote_hub, outlet_wireless_dimmer, voice-scene

async def getItemBindings(self, item_id)

Returns a JSON list of the bindings information for the specified item.

Parameters

item_id - The Control4 item ID.

async def getItemCommands(self, item_id)

Returns a JSON list of the commands available for the specified item.

Parameters

item_id - The Control4 item ID.

async def getItemInfo(self, item_id)

Returns a JSON list of the details of the specified item.

Parameters

item_id - The Control4 item ID.

async def getItemNetwork(self, item_id)

Returns a JSON list of the network information for the specified item.

Parameters

item_id - The Control4 item ID.

async def getItemSetup(self, item_id)

Returns a JSON list of the setup info of the specified item.

Parameters

item_id - The Control4 item ID.

async def getItemVariableValue(self, item_id, var_name)

Returns the value of the specified variable for the specified item as a string.

Parameters

item_id - The Control4 item ID.

var_name - The Control4 variable name or names.

async def getItemVariables(self, item_id)

Returns a JSON list of the variables available for the specified item.

Parameters

item_id - The Control4 item ID.

async def getUiConfiguration(self)

Returns a dictionary of the JSON Control4 App UI Configuration enumerating rooms and capabilities

Returns

{ "experiences": [ { "type": "watch", "sources": { "source": [ { "id": 59, "type": "HDMI" }, { "id": 946, "type": "HDMI" }, { "id": 950, "type": "HDMI" }, { "id": 33, "type": "VIDEO_SELECTION" } ] }, "active": false, "room_id": 9, "username": "primaryuser" }, { "type": "listen", "sources": { "source": [ { "id": 298, "type": "DIGITAL_AUDIO_SERVER", "name": "My Music" }, { "id": 302, "type": "AUDIO_SELECTION", "name": "Stations" }, { "id": 306, "type": "DIGITAL_AUDIO_SERVER", "name": "ShairBridge" }, { "id": 937, "type": "DIGITAL_AUDIO_SERVER", "name": "Spotify Connect" }, { "id": 100002, "type": "DIGITAL_AUDIO_CLIENT", "name": "Digital Media" } ] }, "active": false, "room_id": 9, "username": "primaryuser" }, { "type": "cameras", "sources": { "source": [ { "id": 877, "type": "Camera" }, … } … }

async def sendGetRequest(self, uri)

Sends a GET request to the specified API URI. Returns the Director's JSON response as a string.

Parameters

uri - The API URI to send the request to. Do not include the IP address of the Director.

async def sendPostRequest(self, uri, command, params, async_variable=True)

Sends a POST request to the specified API URI. Used to send commands to the Director. Returns the Director's JSON response as a string.

Parameters

uri - The API URI to send the request to. Do not include the IP address of the Director.

command - The Control4 command to send.

params - The parameters of the command, provided as a dictionary.

pyControl4-1.2.1/docs/error_handling.html000066400000000000000000000334121466515773400204300ustar00rootroot00000000000000 pyControl4.error_handling API documentation

Module pyControl4.error_handling

Handles errors recieved from the Control4 API.

Functions

async def checkResponseForError(response_text: str)

Checks a string response from the Control4 API for error codes.

Parameters

response_text - JSON or XML response from Control4, as a string.

Classes

class BadCredentials (message)

Raised when provided credentials are incorrect.

Expand source code
class BadCredentials(Unauthorized):
    """Raised when provided credentials are incorrect."""

Ancestors

class BadToken (message)

Raised when director bearer token is invalid.

Expand source code
class BadToken(Unauthorized):
    """Raised when director bearer token is invalid."""

Ancestors

class C4Exception (message)

Base error for pyControl4.

Expand source code
class C4Exception(Exception):
    """Base error for pyControl4."""

    def __init__(self, message):
        self.message = message

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses

class InvalidCategory (message)

Raised when an invalid category is provided when calling C4Director.getAllItemsByCategory().

Expand source code
class InvalidCategory(C4Exception):
    """Raised when an invalid category is provided when calling
    `pyControl4.director.C4Director.getAllItemsByCategory`."""

Ancestors

  • C4Exception
  • builtins.Exception
  • builtins.BaseException
class NotFound (message)

Raised when a 404 response is recieved from the Control4 API. Occurs when the requested controller, etc. could not be found.

Expand source code
class NotFound(C4Exception):
    """Raised when a 404 response is recieved from the Control4 API.
    Occurs when the requested controller, etc. could not be found."""

Ancestors

  • C4Exception
  • builtins.Exception
  • builtins.BaseException
class Unauthorized (message)

Raised when unauthorized, but no other recognized details are provided. Occurs when token is invalid.

Expand source code
class Unauthorized(C4Exception):
    """Raised when unauthorized, but no other recognized details are provided.
    Occurs when token is invalid."""

Ancestors

  • C4Exception
  • builtins.Exception
  • builtins.BaseException

Subclasses

pyControl4-1.2.1/docs/index.html000066400000000000000000000257341466515773400165520ustar00rootroot00000000000000 pyControl4 API documentation

Package pyControl4

Sub-modules

pyControl4.account

Authenticates with the Control4 API, retrieves account and registered controller info, and retrieves a bearer token for connecting to a Control4 …

pyControl4.alarm

Controls Control4 security panel and contact sensor (door, window, motion) devices.

pyControl4.blind

Controls Control4 blind devices.

pyControl4.director

Handles communication with a Control4 Director, and provides functions for getting details about items on the Director.

pyControl4.error_handling

Handles errors recieved from the Control4 API.

pyControl4.light

Controls Control4 Light devices.

pyControl4.relay

Controls Control4 Relay devices. These can include locks, and potentially other types of devices.

pyControl4.room

Controls Control4 Room devices.

pyControl4.websocket

Handles Websocket connections to a Control4 Director, allowing for real-time updates using callbacks.

Classes

class C4Entity (C4Director, item_id)

Creates a Control4 object.

Parameters

C4Director - A C4Director object that corresponds to the Control4 Director that the device is connected to.

item_id - The Control4 item ID.

Expand source code
class C4Entity:
    def __init__(self, C4Director, item_id):
        """Creates a Control4 object.

        Parameters:
            `C4Director` - A `pyControl4.director.C4Director` object that corresponds to the Control4 Director that the device is connected to.

            `item_id` - The Control4 item ID.
        """
        self.director = C4Director
        self.item_id = int(item_id)

Subclasses

pyControl4-1.2.1/docs/light.html000066400000000000000000000261471466515773400165510ustar00rootroot00000000000000 pyControl4.light API documentation

Module pyControl4.light

Controls Control4 Light devices.

Classes

class C4Light (C4Director, item_id)

Creates a Control4 object.

Parameters

C4Director - A C4Director object that corresponds to the Control4 Director that the device is connected to.

item_id - The Control4 item ID.

Expand source code
class C4Light(C4Entity):
    async def getLevel(self):
        """Returns the level of a dimming-capable light as an int 0-100.
        Will cause an error if called on a non-dimmer switch. Use `getState()` instead.
        """
        value = await self.director.getItemVariableValue(self.item_id, "LIGHT_LEVEL")
        return int(value)

    async def getState(self):
        """Returns the power state of a dimmer or switch as a boolean (True=on, False=off)."""
        value = await self.director.getItemVariableValue(self.item_id, "LIGHT_STATE")
        return bool(value)

    async def setLevel(self, level):
        """Sets the light level of a dimmer or turns on/off a switch.
        Any `level > 0` will turn on a switch, and `level = 0` will turn off a switch.

        Parameters:
            `level` - (int) 0-100
        """
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "SET_LEVEL",
            {"LEVEL": level},
        )

    async def rampToLevel(self, level, time):
        """Ramps the light level of a dimmer over time.
        Any `level > 0` will turn on a switch, and `level = 0` will turn off a switch.

        Parameters:
            `level` - (int) 0-100

            `time` - (int) Duration in milliseconds
        """
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "RAMP_TO_LEVEL",
            {"LEVEL": level, "TIME": time},
        )

Ancestors

Methods

async def getLevel(self)

Returns the level of a dimming-capable light as an int 0-100. Will cause an error if called on a non-dimmer switch. Use getState() instead.

async def getState(self)

Returns the power state of a dimmer or switch as a boolean (True=on, False=off).

async def rampToLevel(self, level, time)

Ramps the light level of a dimmer over time. Any level > 0 will turn on a switch, and level = 0 will turn off a switch.

Parameters

level - (int) 0-100

time - (int) Duration in milliseconds

async def setLevel(self, level)

Sets the light level of a dimmer or turns on/off a switch. Any level > 0 will turn on a switch, and level = 0 will turn off a switch.

Parameters

level - (int) 0-100

pyControl4-1.2.1/docs/relay.html000066400000000000000000000312251466515773400165470ustar00rootroot00000000000000 pyControl4.relay API documentation

Module pyControl4.relay

Controls Control4 Relay devices. These can include locks, and potentially other types of devices.

Classes

class C4Relay (C4Director, item_id)

Creates a Control4 object.

Parameters

C4Director - A C4Director object that corresponds to the Control4 Director that the device is connected to.

item_id - The Control4 item ID.

Expand source code
class C4Relay(C4Entity):
    async def getRelayState(self):
        """Returns the current state of the relay.

        For locks, `0` means locked and `1` means unlocked.
        For relays in general, `0` probably means open and `1` probably means closed.
        """

        return await self.director.getItemVariableValue(self.item_id, "RelayState")

    async def getRelayStateVerified(self):
        """Returns True if Relay is functional.

        Notes:
            I think this is just used to verify that the relay is functional,
            not 100% sure though.
        """
        return bool(
            await self.director.getItemVariableValue(self.item_id, "StateVerified")
        )

    async def open(self):
        """Set the relay to its open state.

        Example description JSON for this command from the director:
        ```
        {
          "display": "Lock the Front › Door Lock",
          "command": "OPEN",
          "deviceId": 307
        }
        ```
        """

        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "OPEN",
            {},
        )

    async def close(self):
        """Set the relay to its closed state.

        Example description JSON for this command from the director:
        ```
        {
          "display": "Unlock the Front › Door Lock",
          "command": "CLOSE",
          "deviceId": 307
        }
        ```
        """

        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "CLOSE",
            {},
        )

    async def toggle(self):
        """Toggles the relay state.

        Example description JSON for this command from the director:
        ```
        {
          "display": "Toggle the Front › Door Lock",
          "command": "TOGGLE",
          "deviceId": 307
        }
        ```
        """

        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "TOGGLE",
            {},
        )

Ancestors

Methods

async def close(self)

Set the relay to its closed state.

Example description JSON for this command from the director:

{
  "display": "Unlock the Front › Door Lock",
  "command": "CLOSE",
  "deviceId": 307
}
async def getRelayState(self)

Returns the current state of the relay.

For locks, 0 means locked and 1 means unlocked. For relays in general, 0 probably means open and 1 probably means closed.

async def getRelayStateVerified(self)

Returns True if Relay is functional.

Notes

I think this is just used to verify that the relay is functional, not 100% sure though.

async def open(self)

Set the relay to its open state.

Example description JSON for this command from the director:

{
  "display": "Lock the Front › Door Lock",
  "command": "OPEN",
  "deviceId": 307
}
async def toggle(self)

Toggles the relay state.

Example description JSON for this command from the director:

{
  "display": "Toggle the Front › Door Lock",
  "command": "TOGGLE",
  "deviceId": 307
}
pyControl4-1.2.1/docs/room.html000066400000000000000000000504011466515773400164040ustar00rootroot00000000000000 pyControl4.room API documentation

Module pyControl4.room

Controls Control4 Room devices.

Classes

class C4Room (C4Director, item_id)

A media-oriented view of a Control4 Room, supporting items of type="room"

Creates a Control4 object.

Parameters

C4Director - A C4Director object that corresponds to the Control4 Director that the device is connected to.

item_id - The Control4 item ID.

Expand source code
class C4Room(C4Entity):
    """
    A media-oriented view of a Control4 Room, supporting items of type="room"
    """

    async def isRoomHidden(self) -> bool:
        """Returns True if the room is hidden from the end-user"""
        value = await self.director.getItemVariableValue(self.item_id, "ROOM_HIDDEN")
        return int(value) != 0

    async def isOn(self) -> bool:
        """Returns True/False if the room is "ON" from the director's perspective"""
        value = await self.director.getItemVariableValue(self.item_id, "POWER_STATE")
        return int(value) != 0

    async def setRoomOff(self):
        """Turn the room "OFF" """
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "ROOM_OFF",
            {},
        )

    async def _setSource(self, source_id: int, audio_only: bool):
        """
        Sets the room source, turning on the room if necessary.
        If audio_only, only the current audio device is changed
        """
        await self.director.sendPostRequest(
            f"/api/v1/items/{self.item_id}/commands",
            "SELECT_AUDIO_DEVICE" if audio_only else "SELECT_VIDEO_DEVICE",
            {"deviceid": source_id},
        )

    async def setAudioSource(self, source_id: int):
        """Sets the current audio source for the room"""
        await self._setSource(source_id, audio_only=True)

    async def setVideoAndAudioSource(self, source_id: int):
        """Sets the current audio and video source for the room"""
        await self._setSource(source_id, audio_only=False)

    async def getVolume(self) -> int:
        """Returns the current volume for the room from 0-100"""
        value = await self.director.getItemVariableValue(self.item_id, "CURRENT_VOLUME")
        return int(value)

    async def isMuted(self) -> bool:
        """Returns True if the room is muted"""
        value = await self.director.getItemVariableValue(self.item_id, "IS_MUTED")
        return int(value) != 0

    async def setMuteOn(self):
        """Mute the room"""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "MUTE_ON",
            {},
        )

    async def setMuteOff(self):
        """Unmute the room"""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "MUTE_OFF",
            {},
        )

    async def toggleMute(self):
        """Toggle the current mute state for the room"""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "MUTE_TOGGLE",
            {},
        )

    async def setVolume(self, volume: int):
        """Set the room volume, 0-100"""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "SET_VOLUME_LEVEL",
            {"LEVEL": volume},
        )

    async def setIncrementVolume(self):
        """Decrease volume by 1"""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "PULSE_VOL_UP",
            {},
        )

    async def setDecrementVolume(self):
        """Decrease volume by 1"""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "PULSE_VOL_DOWN",
            {},
        )

    async def setPlay(self):
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "PLAY",
            {},
        )

    async def setPause(self):
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "PAUSE",
            {},
        )

    async def setStop(self):
        """Stops the currently playing media but does not turn off the room"""
        await self.director.sendPostRequest(
            "/api/v1/items/{}/commands".format(self.item_id),
            "STOP",
            {},
        )

    async def getAudioDevices(self):
        """
        Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions

        Get the audio devices located in the room.
        Note that this is literally the devices in the room,
        not necessarily all devices _playable_ in the room.
        See C4Director.getUiConfiguration for a more accurate list
        """
        await self.director.sendGetRequest(
            "/api/v1/locations/rooms/{}/audio_devices".format(self.item_id)
        )

    async def getVideoDevices(self):
        """
        Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions

        Get the video devices located in the room.
        Note that this is literally the devices in the room,
        not necessarily all devices _playable_ in the room.
        See C4Director.getUiConfiguration for a more accurate list
        """
        await self.director.sendGetRequest(
            "/api/v1/locations/rooms/{}/video_devices".format(self.item_id)
        )

Ancestors

Methods

async def getAudioDevices(self)

Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions

Get the audio devices located in the room. Note that this is literally the devices in the room, not necessarily all devices playable in the room. See C4Director.getUiConfiguration for a more accurate list

async def getVideoDevices(self)

Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions

Get the video devices located in the room. Note that this is literally the devices in the room, not necessarily all devices playable in the room. See C4Director.getUiConfiguration for a more accurate list

async def getVolume(self) ‑> int

Returns the current volume for the room from 0-100

async def isMuted(self) ‑> bool

Returns True if the room is muted

async def isOn(self) ‑> bool

Returns True/False if the room is "ON" from the director's perspective

async def isRoomHidden(self) ‑> bool

Returns True if the room is hidden from the end-user

async def setAudioSource(self, source_id: int)

Sets the current audio source for the room

async def setDecrementVolume(self)

Decrease volume by 1

async def setIncrementVolume(self)

Decrease volume by 1

async def setMuteOff(self)

Unmute the room

async def setMuteOn(self)

Mute the room

async def setPause(self)
async def setPlay(self)
async def setRoomOff(self)

Turn the room "OFF"

async def setStop(self)

Stops the currently playing media but does not turn off the room

async def setVideoAndAudioSource(self, source_id: int)

Sets the current audio and video source for the room

async def setVolume(self, volume: int)

Set the room volume, 0-100

async def toggleMute(self)

Toggle the current mute state for the room

pyControl4-1.2.1/docs/websocket.html000066400000000000000000000421071466515773400174220ustar00rootroot00000000000000 pyControl4.websocket API documentation

Module pyControl4.websocket

Handles Websocket connections to a Control4 Director, allowing for real-time updates using callbacks.

Classes

class C4Websocket (ip, session_no_verify_ssl: aiohttp.client.ClientSession = None, connect_callback=None, disconnect_callback=None)

Creates a Control4 Websocket object.

Parameters

ip - The IP address of the Control4 Director/Controller.

session - (Optional) Allows the use of an aiohttp.ClientSession object for all network requests. This session will not be closed by the library. If not provided, the library will open and close its own ClientSessions as needed.

connect_callback - (Optional) A callback to be called when the Websocket connection is opened or reconnected after a network error.

disconnect_callback - (Optional) A callback to be called when the Websocket connection is lost due to a network error.

Expand source code
class C4Websocket:
    def __init__(
        self,
        ip,
        session_no_verify_ssl: aiohttp.ClientSession = None,
        connect_callback=None,
        disconnect_callback=None,
    ):
        """Creates a Control4 Websocket object.

        Parameters:
            `ip` - The IP address of the Control4 Director/Controller.

            `session` - (Optional) Allows the use of an
                        `aiohttp.ClientSession` object
                        for all network requests. This
                        session will not be closed by the library.
                        If not provided, the library will open and
                        close its own `ClientSession`s as needed.

            `connect_callback` - (Optional) A callback to be called when the Websocket connection is opened or reconnected after a network error.

            `disconnect_callback` - (Optional) A callback to be called when the Websocket connection is lost due to a network error.
        """
        self.base_url = "https://{}".format(ip)
        self.wss_url = "wss://{}".format(ip)
        self.session = session_no_verify_ssl
        self.connect_callback = connect_callback
        self.disconnect_callback = disconnect_callback

        # Keep track of the callbacks registered for each item id
        self._item_callbacks = dict()
        # Initialize self._sio to None
        self._sio = None

    @property
    def item_callbacks(self):
        """Returns a dictionary of registered item ids (key) and their callbacks (value).

        item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead.
        """
        return self._item_callbacks

    def add_item_callback(self, item_id, callback):
        """Register a callback to receive updates about an item.
        If a callback is already registered for the item, it will be overwritten with the provided callback.

        Parameters:
            `item_id` - The Control4 item ID.

            `callback` - The callback to be called when an update is received for the provided item id.
        """

        _LOGGER.debug("Subscribing to updates for item id: %s", item_id)

        self._item_callbacks[item_id] = callback

    def remove_item_callback(self, item_id):
        """Unregister callback for an item.

        Parameters:
            `item_id` - The Control4 item ID.
        """
        self._item_callbacks.pop(item_id)

    async def sio_connect(self, director_bearer_token):
        """Start WebSockets connection and listen, using the provided director_bearer_token to authenticate with the Control4 Director.
        If a connection already exists, it will be disconnected and a new connection will be created.

        This function should be called using a new token every 86400 seconds (the expiry time of the director tokens), otherwise the Control4 Director will stop sending WebSocket messages.

        Parameters:
            `director_bearer_token` - The bearer token used to authenticate with the Director. See `pyControl4.account.C4Account.getDirectorBearerToken` for how to get this.
        """
        # Disconnect previous sio object
        await self.sio_disconnect()

        self._sio = socketio.AsyncClient(ssl_verify=False)
        self._sio.register_namespace(
            _C4DirectorNamespace(
                token=director_bearer_token,
                url=self.base_url,
                callback=self._callback,
                session=self.session,
                connect_callback=self.connect_callback,
                disconnect_callback=self.disconnect_callback,
            )
        )
        await self._sio.connect(
            self.wss_url,
            transports=["websocket"],
            headers={"JWT": director_bearer_token},
        )

    async def sio_disconnect(self):
        """Disconnects the WebSockets connection, if it has been created."""
        if isinstance(self._sio, socketio.AsyncClient):
            await self._sio.disconnect()

    async def _callback(self, message):
        if "status" in message:
            _LOGGER.debug(f'Subscription {message["status"]}')
            return True
        if isinstance(message, list):
            for m in message:
                await self._process_message(m)
        else:
            await self._process_message(message)

    async def _process_message(self, message):
        """Process an incoming event message."""
        _LOGGER.debug(message)
        try:
            c = self._item_callbacks[message["iddevice"]]
        except KeyError:
            _LOGGER.debug("No Callback for device id {}".format(message["iddevice"]))
            return True

        if isinstance(message, list):
            for m in message:
                await c(message["iddevice"], m)
        else:
            await c(message["iddevice"], message)

    async def _execute_callback(self, callback, *args, **kwargs):
        """Callback with some data capturing any excpetions."""
        try:
            self.sio.emit("ping")
            await callback(*args, **kwargs)
        except Exception as exc:
            _LOGGER.warning("Captured exception during callback: {}".format(str(exc)))

Instance variables

prop item_callbacks

Returns a dictionary of registered item ids (key) and their callbacks (value).

item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead.

Expand source code
@property
def item_callbacks(self):
    """Returns a dictionary of registered item ids (key) and their callbacks (value).

    item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead.
    """
    return self._item_callbacks

Methods

def add_item_callback(self, item_id, callback)

Register a callback to receive updates about an item. If a callback is already registered for the item, it will be overwritten with the provided callback.

Parameters

item_id - The Control4 item ID.

callback - The callback to be called when an update is received for the provided item id.

def remove_item_callback(self, item_id)

Unregister callback for an item.

Parameters

item_id - The Control4 item ID.

async def sio_connect(self, director_bearer_token)

Start WebSockets connection and listen, using the provided director_bearer_token to authenticate with the Control4 Director. If a connection already exists, it will be disconnected and a new connection will be created.

This function should be called using a new token every 86400 seconds (the expiry time of the director tokens), otherwise the Control4 Director will stop sending WebSocket messages.

Parameters

director_bearer_token - The bearer token used to authenticate with the Director. See C4Account.getDirectorBearerToken() for how to get this.

async def sio_disconnect(self)

Disconnects the WebSockets connection, if it has been created.

pyControl4-1.2.1/pyControl4/000077500000000000000000000000001466515773400156675ustar00rootroot00000000000000pyControl4-1.2.1/pyControl4/__init__.py000066400000000000000000000006121466515773400177770ustar00rootroot00000000000000class C4Entity: def __init__(self, C4Director, item_id): """Creates a Control4 object. Parameters: `C4Director` - A `pyControl4.director.C4Director` object that corresponds to the Control4 Director that the device is connected to. `item_id` - The Control4 item ID. """ self.director = C4Director self.item_id = int(item_id) pyControl4-1.2.1/pyControl4/account.py000066400000000000000000000230401466515773400176740ustar00rootroot00000000000000"""Authenticates with the Control4 API, retrieves account and registered controller info, and retrieves a bearer token for connecting to a Control4 Director. """ import aiohttp import async_timeout import json import logging import datetime from .error_handling import checkResponseForError AUTHENTICATION_ENDPOINT = "https://apis.control4.com/authentication/v1/rest" CONTROLLER_AUTHORIZATION_ENDPOINT = ( "https://apis.control4.com/authentication/v1/rest/authorization" ) GET_CONTROLLERS_ENDPOINT = "https://apis.control4.com/account/v3/rest/accounts" APPLICATION_KEY = "78f6791373d61bea49fdb9fb8897f1f3af193f11" _LOGGER = logging.getLogger(__name__) class C4Account: def __init__( self, username, password, session: aiohttp.ClientSession = None, ): """Creates a Control4 account object. Parameters: `username` - Control4 account username/email. `password` - Control4 account password. `session` - (Optional) Allows the use of an `aiohttp.ClientSession` object for all network requests. This session will not be closed by the library. If not provided, the library will open and close its own `ClientSession`s as needed. """ self.username = username self.password = password self.session = session async def __sendAccountAuthRequest(self): """Used internally to retrieve an account bearer token. Returns the entire JSON response from the Control4 auth API. """ dataDictionary = { "clientInfo": { "device": { "deviceName": "pyControl4", "deviceUUID": "0000000000000000", "make": "pyControl4", "model": "pyControl4", "os": "Android", "osVersion": "10", }, "userInfo": { "applicationKey": APPLICATION_KEY, "password": self.password, "userName": self.username, }, } } if self.session is None: async with aiohttp.ClientSession() as session: with async_timeout.timeout(10): async with session.post( AUTHENTICATION_ENDPOINT, json=dataDictionary ) as resp: await checkResponseForError(await resp.text()) return await resp.text() else: with async_timeout.timeout(10): async with self.session.post( AUTHENTICATION_ENDPOINT, json=dataDictionary ) as resp: await checkResponseForError(await resp.text()) return await resp.text() async def __sendAccountGetRequest(self, uri): """Used internally to send GET requests to the Control4 API, authenticated with the account bearer token. Returns the entire JSON response from the Control4 auth API. Parameters: `uri` - Full URI to send GET request to. """ try: headers = {"Authorization": "Bearer {}".format(self.account_bearer_token)} except AttributeError: msg = "The account bearer token is missing - was your username/password correct? " _LOGGER.error(msg) raise if self.session is None: async with aiohttp.ClientSession() as session: with async_timeout.timeout(10): async with session.get(uri, headers=headers) as resp: await checkResponseForError(await resp.text()) return await resp.text() else: with async_timeout.timeout(10): async with self.session.get(uri, headers=headers) as resp: await checkResponseForError(await resp.text()) return await resp.text() async def __sendControllerAuthRequest(self, controller_common_name): """Used internally to retrieve an director bearer token. Returns the entire JSON response from the Control4 auth API. Parameters: `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details. """ try: headers = {"Authorization": "Bearer {}".format(self.account_bearer_token)} except AttributeError: msg = "The account bearer token is missing - was your username/password correct? " _LOGGER.error(msg) raise dataDictionary = { "serviceInfo": { "commonName": controller_common_name, "services": "director", } } if self.session is None: async with aiohttp.ClientSession() as session: with async_timeout.timeout(10): async with session.post( CONTROLLER_AUTHORIZATION_ENDPOINT, headers=headers, json=dataDictionary, ) as resp: await checkResponseForError(await resp.text()) return await resp.text() else: with async_timeout.timeout(10): async with self.session.post( CONTROLLER_AUTHORIZATION_ENDPOINT, headers=headers, json=dataDictionary, ) as resp: await checkResponseForError(await resp.text()) return await resp.text() async def getAccountBearerToken(self): """Gets an account bearer token for making Control4 online API requests.""" data = await self.__sendAccountAuthRequest() jsonDictionary = json.loads(data) try: self.account_bearer_token = jsonDictionary["authToken"]["token"] return self.account_bearer_token except KeyError: msg = "Did not recieve an account bearer token. Is your username/password correct? " _LOGGER.error(msg + data) raise async def getAccountControllers(self): """Returns a dictionary of the information for all controllers registered to an account. Returns: ``` { "controllerCommonName": "control4_MODEL_MACADDRESS", "href": "https://apis.control4.com/account/v3/rest/accounts/000000", "name": "Name" } ``` """ data = await self.__sendAccountGetRequest(GET_CONTROLLERS_ENDPOINT) jsonDictionary = json.loads(data) return jsonDictionary["account"] async def getControllerInfo(self, controller_href): """Returns a dictionary of the information of a specific controller. Parameters: `controller_href` - The API `href` of the controller (get this from the output of `getAccountControllers()`) Returns: ``` { 'allowsPatching': True, 'allowsSupport': False, 'blockNotifications': False, 'controllerCommonName': 'control4_MODEL_MACADDRESS', 'controller': { 'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/controller' }, 'created': '2017-08-26T18:33:31Z', 'dealer': { 'href': 'https://apis.control4.com/account/v3/rest/dealers/12345' }, 'enabled': True, 'hasLoggedIn': True, 'href': 'https://apis.control4.com/account/v3/rest/accounts/000000', 'id': 000000, 'lastCheckIn': '2020-06-13T21:52:34Z', 'licenses': { 'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/licenses' }, 'modified': '2020-06-13T21:52:34Z', 'name': 'Name', 'provisionDate': '2017-08-26T18:35:11Z', 'storage': { 'href': 'https://apis.control4.com/storage/v1/rest/accounts/000000' }, 'type': 'Consumer', 'users': { 'href': 'https://apis.control4.com/account/v3/rest/accounts/000000/users' } } ``` """ data = await self.__sendAccountGetRequest(controller_href) jsonDictionary = json.loads(data) return jsonDictionary async def getControllerOSVersion(self, controller_href): """Returns the OS version of a controller as a string. Parameters: `controller_href` - The API `href` of the controller (get this from the output of `getAccountControllers()`) """ data = await self.__sendAccountGetRequest(controller_href + "/controller") jsonDictionary = json.loads(data) return jsonDictionary["osVersion"] async def getDirectorBearerToken(self, controller_common_name): """Returns a dictionary with a director bearer token for making Control4 Director API requests, and its time valid in seconds (usually 86400 seconds) Parameters: `controller_common_name`: Common name of the controller. See `getAccountControllers()` for details. """ data = await self.__sendControllerAuthRequest(controller_common_name) jsonDictionary = json.loads(data) return { "token": jsonDictionary["authToken"]["token"], "validSeconds": jsonDictionary["authToken"]["validSeconds"], } pyControl4-1.2.1/pyControl4/alarm.py000066400000000000000000000157621466515773400173500ustar00rootroot00000000000000"""Controls Control4 security panel and contact sensor (door, window, motion) devices. """ import json from pyControl4 import C4Entity class C4SecurityPanel(C4Entity): async def getArmState(self): """Returns the arm state of the security panel as "DISARMED", "ARMED_HOME", or "ARMED_AWAY".""" disarmed = await self.director.getItemVariableValue( self.item_id, "DISARMED_STATE" ) armed_home = await self.director.getItemVariableValue( self.item_id, "HOME_STATE" ) armed_away = await self.director.getItemVariableValue( self.item_id, "AWAY_STATE" ) if disarmed == 1: return "DISARMED" elif armed_home == 1: return "ARMED_HOME" elif armed_away == 1: return "ARMED_AWAY" async def getAlarmState(self): """Returns `True` if alarm is triggered, otherwise returns `False`.""" alarm_state = await self.director.getItemVariableValue( self.item_id, "ALARM_STATE" ) return bool(alarm_state) async def getDisplayText(self): """Returns the display text of the security panel.""" display_text = await self.director.getItemVariableValue( self.item_id, "DISPLAY_TEXT" ) return display_text async def getTroubleText(self): """Returns the trouble display text of the security panel.""" trouble_text = await self.director.getItemVariableValue( self.item_id, "TROUBLE_TEXT" ) return trouble_text async def getPartitionState(self): """Returns the partition state of the security panel. Possible values include "DISARMED_NOT_READY", "DISARMED_READY", "ARMED_HOME", "ARMED_AWAY", "EXIT_DELAY", "ENTRY_DELAY" """ partition_state = await self.director.getItemVariableValue( self.item_id, "PARTITION_STATE" ) return partition_state async def getDelayTimeTotal(self): """Returns the total exit delay time. Returns 0 if an exit delay is not currently running.""" delay_time_total = await self.director.getItemVariableValue( self.item_id, "DELAY_TIME_TOTAL" ) return delay_time_total async def getDelayTimeRemaining(self): """Returns the remaining exit delay time. Returns 0 if an exit delay is not currently running.""" delay_time_remaining = await self.director.getItemVariableValue( self.item_id, "DELAY_TIME_REMAINING" ) return delay_time_remaining async def getOpenZoneCount(self): """Returns the number of open/unsecured zones.""" open_zone_count = await self.director.getItemVariableValue( self.item_id, "OPEN_ZONE_COUNT" ) return open_zone_count async def getAlarmType(self): """Returns details about the current alarm type.""" alarm_type = await self.director.getItemVariableValue( self.item_id, "ALARM_TYPE" ) return alarm_type async def getArmedType(self): """Returns details about the current arm type.""" armed_type = await self.director.getItemVariableValue( self.item_id, "ARMED_TYPE" ) return armed_type async def getLastEmergency(self): """Returns details about the last emergency trigger.""" last_emergency = await self.director.getItemVariableValue( self.item_id, "LAST_EMERGENCY" ) return last_emergency async def getLastArmFailure(self): """Returns details about the last arm failure.""" last_arm_failed = await self.director.getItemVariableValue( self.item_id, "LAST_ARM_FAILED" ) return last_arm_failed async def setArm(self, usercode, mode: str): """Arms the security panel with the specified mode. Parameters: `usercode` - PIN/code for arming the system. `mode` - Arm mode to use. This depends on what is supported by the security panel itself. """ usercode = str(usercode) await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "PARTITION_ARM", {"ArmType": mode, "UserCode": usercode}, ) async def setDisarm(self, usercode): """Disarms the security panel. Parameters: `usercode` - PIN/code for disarming the system. """ usercode = str(usercode) await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "PARTITION_DISARM", {"UserCode": usercode}, ) async def getEmergencyTypes(self): """Returns the available emergency types as a list. Possible types are "Fire", "Medical", "Panic", and "Police". """ types_list = [] data = await self.director.getItemInfo(self.item_id) jsonDictionary = json.loads(data) if jsonDictionary[0]["capabilities"]["has_fire"]: types_list.append("Fire") if jsonDictionary[0]["capabilities"]["has_medical"]: types_list.append("Medical") if jsonDictionary[0]["capabilities"]["has_panic"]: types_list.append("Panic") if jsonDictionary[0]["capabilities"]["has_police"]: types_list.append("Police") return types_list async def triggerEmergency(self, usercode, type): """Triggers an emergency of the specified type. Parameters: `usercode` - PIN/code for disarming the system. `type` - Type of emergency: "Fire", "Medical", "Panic", or "Police" """ usercode = str(usercode) await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "PARTITION_DISARM", {"UserCode": usercode}, ) async def sendKeyPress(self, key): """Sends a single keypress to the security panel's virtual keypad (if supported). Parameters: `key` - Keypress to send. Only one key at a time. """ key = str(key) await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "KEY_PRESS", {"KeyName": key}, ) class C4ContactSensor: def __init__(self, C4Director, item_id): """Creates a Control4 Contact Sensor object. Parameters: `C4Director` - A `pyControl4.director.C4Director` object that corresponds to the Control4 Director that the security panel is connected to. `item_id` - The Control4 item ID of the contact sensor. """ self.director = C4Director self.item_id = item_id async def getContactState(self): """Returns `True` if contact is triggered (door/window is closed, motion is detected), otherwise returns `False`.""" contact_state = await self.director.getItemVariableValue( self.item_id, "ContactState" ) return bool(contact_state) pyControl4-1.2.1/pyControl4/blind.py000066400000000000000000000105251466515773400173340ustar00rootroot00000000000000"""Controls Control4 blind devices. """ from pyControl4 import C4Entity class C4Blind(C4Entity): async def getBatteryLevel(self): """Returns the battery of a blind. We currently don't know the range or meaning.""" value = await self.director.getItemVariableValue(self.item_id, "Battery Level") return int(value) async def getClosing(self): """Returns an indication of whether the blind is moving in the closed direction as a boolean (True=closing, False=opening). If the blind is stopped, reports the direction it last moved. """ value = await self.director.getItemVariableValue(self.item_id, "Closing") return bool(value) async def getFullyClosed(self): """Returns an indication of whether the blind is fully closed as a boolean (True=fully closed, False=at least partially open).""" value = await self.director.getItemVariableValue(self.item_id, "Fully Closed") return bool(value) async def getFullyOpen(self): """Returns an indication of whether the blind is fully open as a boolean (True=fully open, False=at least partially closed).""" value = await self.director.getItemVariableValue(self.item_id, "Fully Open") return bool(value) async def getLevel(self): """Returns the level (current position) of a blind as an int 0-100. 0 is fully closed and 100 is fully open. """ value = await self.director.getItemVariableValue(self.item_id, "Level") return int(value) async def getOpen(self): """Returns an indication of whether the blind is open as a boolean (True=open, False=closed). This is true even if the blind is only partially open.""" value = await self.director.getItemVariableValue(self.item_id, "Open") return bool(value) async def getOpening(self): """Returns an indication of whether the blind is moving in the open direction as a boolean (True=opening, False=closing). If the blind is stopped, reports the direction it last moved. """ value = await self.director.getItemVariableValue(self.item_id, "Opening") return bool(value) async def getStopped(self): """Returns an indication of whether the blind is stopped as a boolean (True=stopped, False=moving).""" value = await self.director.getItemVariableValue(self.item_id, "Stopped") return bool(value) async def getTargetLevel(self): """Returns the target level (desired position) of a blind as an int 0-100. The blind will move if this is different from the current level. 0 is fully closed and 100 is fully open. """ value = await self.director.getItemVariableValue(self.item_id, "Target Level") return int(value) async def open(self): """Opens the blind completely.""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "SET_LEVEL_TARGET:LEVEL_TARGET_OPEN", {}, ) async def close(self): """Closes the blind completely.""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "SET_LEVEL_TARGET:LEVEL_TARGET_CLOSED", {}, ) async def setLevelTarget(self, level): """Sets the desired level of a blind; it will start moving towards that level. Level 0 is fully closed and level 100 is fully open. Parameters: `level` - (int) 0-100 """ await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "SET_LEVEL_TARGET", {"LEVEL_TARGET": level}, ) async def stop(self): """Stops the blind if it is moving. Shortly after stopping, the target level will be set to the level the blind had actually reached when it stopped.""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "STOP", {}, ) async def toggle(self): """Toggles the blind between open and closed. Has no effect if the blind is partially open.""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "TOGGLE", {}, ) pyControl4-1.2.1/pyControl4/director.py000066400000000000000000000247411466515773400200640ustar00rootroot00000000000000"""Handles communication with a Control4 Director, and provides functions for getting details about items on the Director. """ import aiohttp import async_timeout import json from .error_handling import checkResponseForError class C4Director: def __init__( self, ip, director_bearer_token, session_no_verify_ssl: aiohttp.ClientSession = None, ): """Creates a Control4 Director object. Parameters: `ip` - The IP address of the Control4 Director/Controller. `director_bearer_token` - The bearer token used to authenticate with the Director. See `pyControl4.account.C4Account.getDirectorBearerToken` for how to get this. `session` - (Optional) Allows the use of an `aiohttp.ClientSession` object for all network requests. This session will not be closed by the library. If not provided, the library will open and close its own `ClientSession`s as needed. """ self.base_url = "https://{}".format(ip) self.headers = {"Authorization": "Bearer {}".format(director_bearer_token)} self.director_bearer_token = director_bearer_token self.session = session_no_verify_ssl async def sendGetRequest(self, uri): """Sends a GET request to the specified API URI. Returns the Director's JSON response as a string. Parameters: `uri` - The API URI to send the request to. Do not include the IP address of the Director. """ if self.session is None: async with aiohttp.ClientSession( connector=aiohttp.TCPConnector(verify_ssl=False) ) as session: with async_timeout.timeout(10): async with session.get( self.base_url + uri, headers=self.headers ) as resp: await checkResponseForError(await resp.text()) return await resp.text() else: with async_timeout.timeout(10): async with self.session.get( self.base_url + uri, headers=self.headers ) as resp: await checkResponseForError(await resp.text()) return await resp.text() async def sendPostRequest(self, uri, command, params, async_variable=True): """Sends a POST request to the specified API URI. Used to send commands to the Director. Returns the Director's JSON response as a string. Parameters: `uri` - The API URI to send the request to. Do not include the IP address of the Director. `command` - The Control4 command to send. `params` - The parameters of the command, provided as a dictionary. """ dataDictionary = { "async": async_variable, "command": command, "tParams": params, } if self.session is None: async with aiohttp.ClientSession( connector=aiohttp.TCPConnector(verify_ssl=False) ) as session: with async_timeout.timeout(10): async with session.post( self.base_url + uri, headers=self.headers, json=dataDictionary ) as resp: await checkResponseForError(await resp.text()) return await resp.text() else: with async_timeout.timeout(10): async with self.session.post( self.base_url + uri, headers=self.headers, json=dataDictionary ) as resp: await checkResponseForError(await resp.text()) return await resp.text() async def getAllItemsByCategory(self, category): """Returns a JSON list of items related to a particular category. Parameters: `category` - Control4 Category Name: controllers, comfort, lights, cameras, sensors, audio_video, motorization, thermostats, motors, control4_remote_hub, outlet_wireless_dimmer, voice-scene """ return_list = await self.sendGetRequest( "/api/v1/categories/{}".format(category) ) return return_list async def getAllItemInfo(self): """Returns a JSON list of all the items on the Director.""" return await self.sendGetRequest("/api/v1/items") async def getItemInfo(self, item_id): """Returns a JSON list of the details of the specified item. Parameters: `item_id` - The Control4 item ID. """ return await self.sendGetRequest("/api/v1/items/{}".format(item_id)) async def getItemSetup(self, item_id): """Returns a JSON list of the setup info of the specified item. Parameters: `item_id` - The Control4 item ID. """ return await self.sendPostRequest( "/api/v1/items/{}/commands".format(item_id), "GET_SETUP", {}, False ) async def getItemVariables(self, item_id): """Returns a JSON list of the variables available for the specified item. Parameters: `item_id` - The Control4 item ID. """ return await self.sendGetRequest("/api/v1/items/{}/variables".format(item_id)) async def getItemVariableValue(self, item_id, var_name): """Returns the value of the specified variable for the specified item as a string. Parameters: `item_id` - The Control4 item ID. `var_name` - The Control4 variable name or names. """ if isinstance(var_name, (tuple, list, set)): var_name = ",".join(var_name) data = await self.sendGetRequest( "/api/v1/items/{}/variables?varnames={}".format(item_id, var_name) ) if data == "[]": raise ValueError( "Empty response recieved from Director! The variable {} \ doesn't seem to exist for item {}.".format( var_name, item_id ) ) jsonDictionary = json.loads(data) return jsonDictionary[0]["value"] async def getAllItemVariableValue(self, var_name): """Returns a dictionary with the values of the specified variable for all items that have it. Parameters: `var_name` - The Control4 variable name or names. """ if isinstance(var_name, (tuple, list, set)): var_name = ",".join(var_name) data = await self.sendGetRequest( "/api/v1/items/variables?varnames={}".format(var_name) ) if data == "[]": raise ValueError( "Empty response recieved from Director! The variable {} \ doesn't seem to exist for any items.".format( var_name ) ) jsonDictionary = json.loads(data) return jsonDictionary async def getItemCommands(self, item_id): """Returns a JSON list of the commands available for the specified item. Parameters: `item_id` - The Control4 item ID. """ return await self.sendGetRequest("/api/v1/items/{}/commands".format(item_id)) async def getItemNetwork(self, item_id): """Returns a JSON list of the network information for the specified item. Parameters: `item_id` - The Control4 item ID. """ return await self.sendGetRequest("/api/v1/items/{}/network".format(item_id)) async def getItemBindings(self, item_id): """Returns a JSON list of the bindings information for the specified item. Parameters: `item_id` - The Control4 item ID. """ return await self.sendGetRequest("/api/v1/items/{}/bindings".format(item_id)) async def getUiConfiguration(self): """Returns a dictionary of the JSON Control4 App UI Configuration enumerating rooms and capabilities Returns: { "experiences": [ { "type": "watch", "sources": { "source": [ { "id": 59, "type": "HDMI" }, { "id": 946, "type": "HDMI" }, { "id": 950, "type": "HDMI" }, { "id": 33, "type": "VIDEO_SELECTION" } ] }, "active": false, "room_id": 9, "username": "primaryuser" }, { "type": "listen", "sources": { "source": [ { "id": 298, "type": "DIGITAL_AUDIO_SERVER", "name": "My Music" }, { "id": 302, "type": "AUDIO_SELECTION", "name": "Stations" }, { "id": 306, "type": "DIGITAL_AUDIO_SERVER", "name": "ShairBridge" }, { "id": 937, "type": "DIGITAL_AUDIO_SERVER", "name": "Spotify Connect" }, { "id": 100002, "type": "DIGITAL_AUDIO_CLIENT", "name": "Digital Media" } ] }, "active": false, "room_id": 9, "username": "primaryuser" }, { "type": "cameras", "sources": { "source": [ { "id": 877, "type": "Camera" }, ... } ... } """ return await self.sendGetRequest("/api/v1/agents/ui_configuration") pyControl4-1.2.1/pyControl4/error_handling.py000066400000000000000000000074321466515773400212440ustar00rootroot00000000000000"""Handles errors recieved from the Control4 API.""" import json import xmltodict class C4Exception(Exception): """Base error for pyControl4.""" def __init__(self, message): self.message = message class NotFound(C4Exception): """Raised when a 404 response is recieved from the Control4 API. Occurs when the requested controller, etc. could not be found.""" class Unauthorized(C4Exception): """Raised when unauthorized, but no other recognized details are provided. Occurs when token is invalid.""" class BadCredentials(Unauthorized): """Raised when provided credentials are incorrect.""" class BadToken(Unauthorized): """Raised when director bearer token is invalid.""" class InvalidCategory(C4Exception): """Raised when an invalid category is provided when calling `pyControl4.director.C4Director.getAllItemsByCategory`.""" ERROR_CODES = {"401": Unauthorized, "404": NotFound} ERROR_DETAILS = { "Permission denied Bad credentials": BadCredentials, } DIRECTOR_ERRORS = {"Unauthorized": Unauthorized, "Invalid category": InvalidCategory} DIRECTOR_ERROR_DETAILS = {"Expired or invalid token": BadToken} async def __checkResponseFormat(response_text: str): """Known Control4 authentication API error message formats: ```json { "C4ErrorResponse": { "code": 401, "details": "Permission denied Bad credentials", "message": "Permission denied", "subCode": 0 } } ``` ```json { "code": 404, "details": "Account with id:000000 not found in DB", "message": "Account not found", "subCode": 0 }``` ```xml 401
Permission denied 0
``` Known Control4 director error message formats: ```json { "error": "Unauthorized", "details": "Expired or invalid token" } ``` """ if response_text.startswith("<"): return "XML" return "JSON" async def checkResponseForError(response_text: str): """Checks a string response from the Control4 API for error codes. Parameters: `response_text` - JSON or XML response from Control4, as a string. """ if await __checkResponseFormat(response_text) == "JSON": dictionary = json.loads(response_text) elif await __checkResponseFormat(response_text) == "XML": dictionary = xmltodict.parse(response_text) if "C4ErrorResponse" in dictionary: if ( "details" in dictionary["C4ErrorResponse"] and dictionary["C4ErrorResponse"]["details"] in ERROR_DETAILS ): exception = ERROR_DETAILS.get(dictionary["C4ErrorResponse"]["details"]) raise exception(response_text) else: exception = ERROR_CODES.get( str(dictionary["C4ErrorResponse"]["code"]), C4Exception ) raise exception(response_text) elif "code" in dictionary: if "details" in dictionary and dictionary["details"] in ERROR_DETAILS: exception = ERROR_DETAILS.get(dictionary["details"]) raise exception(response_text) else: exception = ERROR_CODES.get(str(dictionary["code"]), C4Exception) raise exception(response_text) elif "error" in dictionary: if "details" in dictionary and dictionary["details"] in DIRECTOR_ERROR_DETAILS: exception = DIRECTOR_ERROR_DETAILS.get(dictionary["details"]) raise exception(response_text) else: exception = DIRECTOR_ERRORS.get(str(dictionary["error"]), C4Exception) raise exception(response_text) pyControl4-1.2.1/pyControl4/light.py000066400000000000000000000030641466515773400173530ustar00rootroot00000000000000"""Controls Control4 Light devices. """ from pyControl4 import C4Entity class C4Light(C4Entity): async def getLevel(self): """Returns the level of a dimming-capable light as an int 0-100. Will cause an error if called on a non-dimmer switch. Use `getState()` instead. """ value = await self.director.getItemVariableValue(self.item_id, "LIGHT_LEVEL") return int(value) async def getState(self): """Returns the power state of a dimmer or switch as a boolean (True=on, False=off).""" value = await self.director.getItemVariableValue(self.item_id, "LIGHT_STATE") return bool(value) async def setLevel(self, level): """Sets the light level of a dimmer or turns on/off a switch. Any `level > 0` will turn on a switch, and `level = 0` will turn off a switch. Parameters: `level` - (int) 0-100 """ await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "SET_LEVEL", {"LEVEL": level}, ) async def rampToLevel(self, level, time): """Ramps the light level of a dimmer over time. Any `level > 0` will turn on a switch, and `level = 0` will turn off a switch. Parameters: `level` - (int) 0-100 `time` - (int) Duration in milliseconds """ await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "RAMP_TO_LEVEL", {"LEVEL": level, "TIME": time}, ) pyControl4-1.2.1/pyControl4/relay.py000066400000000000000000000042621466515773400173610ustar00rootroot00000000000000"""Controls Control4 Relay devices. These can include locks, and potentially other types of devices. """ from pyControl4 import C4Entity class C4Relay(C4Entity): async def getRelayState(self): """Returns the current state of the relay. For locks, `0` means locked and `1` means unlocked. For relays in general, `0` probably means open and `1` probably means closed. """ return await self.director.getItemVariableValue(self.item_id, "RelayState") async def getRelayStateVerified(self): """Returns True if Relay is functional. Notes: I think this is just used to verify that the relay is functional, not 100% sure though. """ return bool( await self.director.getItemVariableValue(self.item_id, "StateVerified") ) async def open(self): """Set the relay to its open state. Example description JSON for this command from the director: ``` { "display": "Lock the Front › Door Lock", "command": "OPEN", "deviceId": 307 } ``` """ await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "OPEN", {}, ) async def close(self): """Set the relay to its closed state. Example description JSON for this command from the director: ``` { "display": "Unlock the Front › Door Lock", "command": "CLOSE", "deviceId": 307 } ``` """ await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "CLOSE", {}, ) async def toggle(self): """Toggles the relay state. Example description JSON for this command from the director: ``` { "display": "Toggle the Front › Door Lock", "command": "TOGGLE", "deviceId": 307 } ``` """ await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "TOGGLE", {}, ) pyControl4-1.2.1/pyControl4/room.py000066400000000000000000000121141466515773400172140ustar00rootroot00000000000000"""Controls Control4 Room devices. """ from pyControl4 import C4Entity class C4Room(C4Entity): """ A media-oriented view of a Control4 Room, supporting items of type="room" """ async def isRoomHidden(self) -> bool: """Returns True if the room is hidden from the end-user""" value = await self.director.getItemVariableValue(self.item_id, "ROOM_HIDDEN") return int(value) != 0 async def isOn(self) -> bool: """Returns True/False if the room is "ON" from the director's perspective""" value = await self.director.getItemVariableValue(self.item_id, "POWER_STATE") return int(value) != 0 async def setRoomOff(self): """Turn the room "OFF" """ await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "ROOM_OFF", {}, ) async def _setSource(self, source_id: int, audio_only: bool): """ Sets the room source, turning on the room if necessary. If audio_only, only the current audio device is changed """ await self.director.sendPostRequest( f"/api/v1/items/{self.item_id}/commands", "SELECT_AUDIO_DEVICE" if audio_only else "SELECT_VIDEO_DEVICE", {"deviceid": source_id}, ) async def setAudioSource(self, source_id: int): """Sets the current audio source for the room""" await self._setSource(source_id, audio_only=True) async def setVideoAndAudioSource(self, source_id: int): """Sets the current audio and video source for the room""" await self._setSource(source_id, audio_only=False) async def getVolume(self) -> int: """Returns the current volume for the room from 0-100""" value = await self.director.getItemVariableValue(self.item_id, "CURRENT_VOLUME") return int(value) async def isMuted(self) -> bool: """Returns True if the room is muted""" value = await self.director.getItemVariableValue(self.item_id, "IS_MUTED") return int(value) != 0 async def setMuteOn(self): """Mute the room""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "MUTE_ON", {}, ) async def setMuteOff(self): """Unmute the room""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "MUTE_OFF", {}, ) async def toggleMute(self): """Toggle the current mute state for the room""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "MUTE_TOGGLE", {}, ) async def setVolume(self, volume: int): """Set the room volume, 0-100""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "SET_VOLUME_LEVEL", {"LEVEL": volume}, ) async def setIncrementVolume(self): """Decrease volume by 1""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "PULSE_VOL_UP", {}, ) async def setDecrementVolume(self): """Decrease volume by 1""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "PULSE_VOL_DOWN", {}, ) async def setPlay(self): await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "PLAY", {}, ) async def setPause(self): await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "PAUSE", {}, ) async def setStop(self): """Stops the currently playing media but does not turn off the room""" await self.director.sendPostRequest( "/api/v1/items/{}/commands".format(self.item_id), "STOP", {}, ) async def getAudioDevices(self): """ Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions Get the audio devices located in the room. Note that this is literally the devices in the room, not necessarily all devices _playable_ in the room. See C4Director.getUiConfiguration for a more accurate list """ await self.director.sendGetRequest( "/api/v1/locations/rooms/{}/audio_devices".format(self.item_id) ) async def getVideoDevices(self): """ Note: As tested in OS 3.2.3 this doesn't work, but may work in previous versions Get the video devices located in the room. Note that this is literally the devices in the room, not necessarily all devices _playable_ in the room. See C4Director.getUiConfiguration for a more accurate list """ await self.director.sendGetRequest( "/api/v1/locations/rooms/{}/video_devices".format(self.item_id) ) pyControl4-1.2.1/pyControl4/websocket.py000066400000000000000000000211671466515773400202360ustar00rootroot00000000000000"""Handles Websocket connections to a Control4 Director, allowing for real-time updates using callbacks.""" import aiohttp import async_timeout import socketio_v4 as socketio import logging from .error_handling import checkResponseForError _LOGGER = logging.getLogger(__name__) class _C4DirectorNamespace(socketio.AsyncClientNamespace): def __init__(self, *args, **kwargs): self.url = kwargs.pop("url") self.token = kwargs.pop("token") self.callback = kwargs.pop("callback") self.session = kwargs.pop("session") self.connect_callback = kwargs.pop("connect_callback") self.disconnect_callback = kwargs.pop("disconnect_callback") super().__init__(*args, **kwargs) self.uri = "/api/v1/items/datatoui" self.subscriptionId = None self.connected = False async def on_connect(self): _LOGGER.debug("Control4 Director socket.io connection established!") if self.connect_callback is not None: await self.connect_callback() async def on_disconnect(self): self.connected = False self.subscriptionId = None _LOGGER.debug("Control4 Director socket.io disconnected.") if self.disconnect_callback is not None: await self.disconnect_callback() async def trigger_event(self, event, *args): if event == "subscribe": await self.on_subscribe(*args) elif event == "connect": await self.on_connect() elif event == "disconnect": await self.on_disconnect() elif event == "clientId": await self.on_clientId(*args) elif event == self.subscriptionId: msg = args[0] if "status" in msg: _LOGGER.debug(f'Status message received from Director: {msg["status"]}') await self.emit("2") else: await self.callback(args[0]) async def on_clientId(self, clientId): await self.emit("2probe") if not self.connected and not self.subscriptionId: _LOGGER.debug("Fetching subscriptionID from Control4") if self.session is None: async with aiohttp.ClientSession( connector=aiohttp.TCPConnector(verify_ssl=False) ) as session: with async_timeout.timeout(10): async with session.get( self.url + self.uri, params={"JWT": self.token, "SubscriptionClient": clientId}, ) as resp: await checkResponseForError(await resp.text()) data = await resp.json() self.connected = True self.subscriptionId = data["subscriptionId"] await self.emit("startSubscription", self.subscriptionId) else: with async_timeout.timeout(10): async with self.session.get( self.url + self.uri, params={"JWT": self.token, "SubscriptionClient": clientId}, ) as resp: await checkResponseForError(await resp.text()) data = await resp.json() self.connected = True self.subscriptionId = data["subscriptionId"] await self.emit("startSubscription", self.subscriptionId) async def on_subscribe(self, message): await self.message(message) class C4Websocket: def __init__( self, ip, session_no_verify_ssl: aiohttp.ClientSession = None, connect_callback=None, disconnect_callback=None, ): """Creates a Control4 Websocket object. Parameters: `ip` - The IP address of the Control4 Director/Controller. `session` - (Optional) Allows the use of an `aiohttp.ClientSession` object for all network requests. This session will not be closed by the library. If not provided, the library will open and close its own `ClientSession`s as needed. `connect_callback` - (Optional) A callback to be called when the Websocket connection is opened or reconnected after a network error. `disconnect_callback` - (Optional) A callback to be called when the Websocket connection is lost due to a network error. """ self.base_url = "https://{}".format(ip) self.wss_url = "wss://{}".format(ip) self.session = session_no_verify_ssl self.connect_callback = connect_callback self.disconnect_callback = disconnect_callback # Keep track of the callbacks registered for each item id self._item_callbacks = dict() # Initialize self._sio to None self._sio = None @property def item_callbacks(self): """Returns a dictionary of registered item ids (key) and their callbacks (value). item_callbacks cannot be modified directly. Use add_item_callback() and remove_item_callback() instead. """ return self._item_callbacks def add_item_callback(self, item_id, callback): """Register a callback to receive updates about an item. If a callback is already registered for the item, it will be overwritten with the provided callback. Parameters: `item_id` - The Control4 item ID. `callback` - The callback to be called when an update is received for the provided item id. """ _LOGGER.debug("Subscribing to updates for item id: %s", item_id) self._item_callbacks[item_id] = callback def remove_item_callback(self, item_id): """Unregister callback for an item. Parameters: `item_id` - The Control4 item ID. """ self._item_callbacks.pop(item_id) async def sio_connect(self, director_bearer_token): """Start WebSockets connection and listen, using the provided director_bearer_token to authenticate with the Control4 Director. If a connection already exists, it will be disconnected and a new connection will be created. This function should be called using a new token every 86400 seconds (the expiry time of the director tokens), otherwise the Control4 Director will stop sending WebSocket messages. Parameters: `director_bearer_token` - The bearer token used to authenticate with the Director. See `pyControl4.account.C4Account.getDirectorBearerToken` for how to get this. """ # Disconnect previous sio object await self.sio_disconnect() self._sio = socketio.AsyncClient(ssl_verify=False) self._sio.register_namespace( _C4DirectorNamespace( token=director_bearer_token, url=self.base_url, callback=self._callback, session=self.session, connect_callback=self.connect_callback, disconnect_callback=self.disconnect_callback, ) ) await self._sio.connect( self.wss_url, transports=["websocket"], headers={"JWT": director_bearer_token}, ) async def sio_disconnect(self): """Disconnects the WebSockets connection, if it has been created.""" if isinstance(self._sio, socketio.AsyncClient): await self._sio.disconnect() async def _callback(self, message): if "status" in message: _LOGGER.debug(f'Subscription {message["status"]}') return True if isinstance(message, list): for m in message: await self._process_message(m) else: await self._process_message(message) async def _process_message(self, message): """Process an incoming event message.""" _LOGGER.debug(message) try: c = self._item_callbacks[message["iddevice"]] except KeyError: _LOGGER.debug("No Callback for device id {}".format(message["iddevice"])) return True if isinstance(message, list): for m in message: await c(message["iddevice"], m) else: await c(message["iddevice"], message) async def _execute_callback(self, callback, *args, **kwargs): """Callback with some data capturing any excpetions.""" try: self.sio.emit("ping") await callback(*args, **kwargs) except Exception as exc: _LOGGER.warning("Captured exception during callback: {}".format(str(exc))) pyControl4-1.2.1/requirements-dev.txt000066400000000000000000000001151466515773400176470ustar00rootroot00000000000000aiohttp async_timeout xmltodict python-socketio-v4 websocket-client pdoc3 pyControl4-1.2.1/requirements.txt000066400000000000000000000001041466515773400170710ustar00rootroot00000000000000aiohttp async_timeout xmltodict python-socketio-v4 websocket-client pyControl4-1.2.1/setup.py000066400000000000000000000015301466515773400153230ustar00rootroot00000000000000import setuptools with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="pyControl4", # Replace with your own username version="1.2.1", author="lawtancool", author_email="contact@lawrencetan.ca", description="Python 3 asyncio package for interacting with Control4 systems", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/lawtancool/pyControl4", packages=setuptools.find_packages(), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", ], python_requires=">=3.6", install_requires=[ "aiohttp", "xmltodict", "python-socketio-v4", "websocket-client", ], ) pyControl4-1.2.1/test.py000066400000000000000000000032051466515773400151430ustar00rootroot00000000000000from pyControl4.account import C4Account from pyControl4.director import C4Director from pyControl4.light import C4Light from pyControl4.alarm import C4SecurityPanel, C4ContactSensor from pyControl4.error_handling import checkResponseForError from login_info import * import asyncio import json import aiohttp ip = "192.168.1.25" # asyncio.run( # checkResponseForError( # '{"code":404,"details":"Account with id:527154 not found in DB","message":"Account not found","subCode":0}' # ) # ) async def returnClientSession(): session = aiohttp.ClientSession() return session # session = asyncio.run(returnClientSession()) account = C4Account(username, password) asyncio.run(account.getAccountBearerToken()) data = asyncio.run(account.getAccountControllers()) # print(asyncio.run(account.getAccountControllers())) # print(data["controllerCommonName"]) # print(data["href"]) # print(asyncio.run(account.getControllerOSVersion(data["href"]))) director_bearer_token = asyncio.run( account.getDirectorBearerToken(data["controllerCommonName"]) ) # print(director_bearer_token) director = C4Director(ip, director_bearer_token["token"]) alarm = C4SecurityPanel(director, 460) print(asyncio.run(alarm.getEmergencyTypes())) print(asyncio.run(director.getItemSetup(471))) # sensor = C4ContactSensor(director, 471) # print(asyncio.run(sensor.getContactState())) # f = open("allitems.txt", "x") # f.write(asyncio.run(director.getAllItemInfo())) # f.close() # print(asyncio.run(director.getAllItemVariableValue("LIGHT_LEVEL"))) # light = C4Light(director, 253) # asyncio.run(light.rampToLevel(10, 10000)) # print(asyncio.run(light.getState()))