├── __init__.py ├── tests.py ├── .pre-commit-config.yaml ├── Pipfile ├── setup.py ├── README.md ├── .gitignore ├── LICENSE.txt ├── Pipfile.lock └── alpaca.py /__init__.py: -------------------------------------------------------------------------------- 1 | """Python interface for ASCOM Alpaca.""" 2 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | """This module contains test cases for Alpyca.""" 2 | from pytest import fixture 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/python/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | - repo: https://github.com/PyCQA/pydocstyle 7 | rev: 6de6d938377e2db3bfe6f706fa2e3b90c412db76 8 | hooks: 9 | - id: pydocstyle 10 | default_language_version: 11 | python: python3 -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pydocstyle = "*" 8 | black = "==19.3b0" 9 | pre-commit = "*" 10 | rope = "*" 11 | pytest = "*" 12 | 13 | [packages] 14 | requests = "*" 15 | python-dateutil = "*" 16 | 17 | [requires] 18 | python_version = "3.7" 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup Alpyca for distribution.""" 2 | import setuptools 3 | 4 | with open("README.md", "r") as f: 5 | long_description = f.read() 6 | 7 | setuptools.setup( 8 | name="Alpyca", 9 | description="Python interface for ASCOM Alpaca.", 10 | long_description=long_description, 11 | long_description_content_type="text/markdown", 12 | author="Ethan Chappel", 13 | url="https://github.com/EthanChappel/Alpyca", 14 | version="1.0.0b1", 15 | license="LICENSE.txt", 16 | py_modules=["alpaca"], 17 | install_requires=["requests", "python-dateutil"], 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "Development Status :: 4 - Beta", 21 | "Operating System :: OS Independent", 22 | "Intended Audience :: Developers", 23 | "Topic :: Scientific/Engineering :: Astronomy", 24 | "License :: OSI Approved :: Apache Software License", 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alpyca 2 | Python interface for [ASCOM Alpaca](https://ascom-standards.org/Developer/Alpaca.htm) 3 | 4 | **This project is now maintained by the ASCOM Initiative and can be found here: https://github.com/ASCOMInitiative/alpyca/** 5 | 6 | 7 | 8 | ## Install 9 | #### Using Pip 10 | If downloading from the Python Package Index, run 11 | ``` 12 | python3 -m pip install Alpyca 13 | ``` 14 | 15 | #### From source 16 | If you have the source code in a tar file, extract it and run 17 | ``` 18 | python3 setup.py install 19 | ``` 20 | 21 | ## Documentation 22 | All methods have docstrings accessible with Python's built-in [```help()```](https://docs.python.org/3/library/functions.html#help) function. 23 | 24 | Alpyca's classes, methods, and parameters use the same names as ASCOM Alpaca's RESTful API. The documentation for Alpaca can be found at [https://ascom-standards.org/api/](https://ascom-standards.org/api/). 25 | 26 | ### Example 27 | The address to move a telescope accessible at ```http://127.0.0.1:11111/api/v1/telescope/0/moveaxis``` with the request body ```{"Axis": 0, "Rate": 1.5}``` translates into this Python code: 28 | ``` 29 | # Import the Telescope class. 30 | from alpaca import Telescope 31 | 32 | # Initialize a Telescope object with 0 as the device number. 33 | t = Telescope('127.0.0.1:11111', 0) 34 | 35 | # Move the primary axis 1.5 degrees per second. 36 | t.moveaxis(Axis=0, Rate=1.5) 37 | ``` 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | pip-wheel-metadata/ 21 | share/python-wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | MANIFEST 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-delete-this-directory.txt 30 | 31 | # Unit test / coverage reports 32 | htmlcov/ 33 | .tox/ 34 | .nox/ 35 | .coverage 36 | .coverage.* 37 | .cache 38 | nosetests.xml 39 | coverage.xml 40 | *.cover 41 | .hypothesis/ 42 | .pytest_cache/ 43 | 44 | 45 | # Sphinx documentation 46 | docs/_build/ 47 | 48 | # PyBuilder 49 | target/ 50 | 51 | # IPython 52 | profile_default/ 53 | ipython_config.py 54 | 55 | # pyenv 56 | .python-version 57 | 58 | # pipenv 59 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 60 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 61 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 62 | # install all needed dependencies. 63 | #Pipfile.lock 64 | 65 | # Environments 66 | .env 67 | .venv 68 | env/ 69 | venv/ 70 | ENV/ 71 | env.bak/ 72 | venv.bak/ 73 | 74 | # Spyder project settings 75 | .spyderproject 76 | .spyproject 77 | 78 | # Rope project settings 79 | .ropeproject 80 | 81 | # Visual Studio Code project settings 82 | .vscode 83 | 84 | # PyCharm project settings 85 | .idea 86 | 87 | # mypy 88 | .mypy_cache/ 89 | .dmypy.json 90 | dmypy.json 91 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "1831f26cef2c9411b529c2a88e292e9ffae8752551e38ed0e53e78c039949765" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", 22 | "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" 23 | ], 24 | "version": "==2019.3.9" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "idna": { 34 | "hashes": [ 35 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 36 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 37 | ], 38 | "version": "==2.8" 39 | }, 40 | "python-dateutil": { 41 | "hashes": [ 42 | "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", 43 | "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" 44 | ], 45 | "index": "pypi", 46 | "version": "==2.8.0" 47 | }, 48 | "requests": { 49 | "hashes": [ 50 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 51 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 52 | ], 53 | "index": "pypi", 54 | "version": "==2.22.0" 55 | }, 56 | "six": { 57 | "hashes": [ 58 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 59 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 60 | ], 61 | "version": "==1.12.0" 62 | }, 63 | "urllib3": { 64 | "hashes": [ 65 | "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", 66 | "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" 67 | ], 68 | "version": "==1.25.3" 69 | } 70 | }, 71 | "develop": { 72 | "appdirs": { 73 | "hashes": [ 74 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 75 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 76 | ], 77 | "version": "==1.4.3" 78 | }, 79 | "aspy.yaml": { 80 | "hashes": [ 81 | "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc", 82 | "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45" 83 | ], 84 | "version": "==1.3.0" 85 | }, 86 | "atomicwrites": { 87 | "hashes": [ 88 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", 89 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" 90 | ], 91 | "version": "==1.3.0" 92 | }, 93 | "attrs": { 94 | "hashes": [ 95 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", 96 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 97 | ], 98 | "version": "==19.1.0" 99 | }, 100 | "black": { 101 | "hashes": [ 102 | "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", 103 | "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" 104 | ], 105 | "index": "pypi", 106 | "version": "==19.3b0" 107 | }, 108 | "cfgv": { 109 | "hashes": [ 110 | "sha256:32edbe09de6f4521224b87822103a8c16a614d31a894735f7a5b3bcf0eb3c37e", 111 | "sha256:3bd31385cd2bebddbba8012200aaf15aa208539f1b33973759b4d02fc2148da5" 112 | ], 113 | "version": "==2.0.0" 114 | }, 115 | "click": { 116 | "hashes": [ 117 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 118 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 119 | ], 120 | "version": "==7.0" 121 | }, 122 | "colorama": { 123 | "hashes": [ 124 | "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", 125 | "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48" 126 | ], 127 | "markers": "sys_platform == 'win32'", 128 | "version": "==0.4.1" 129 | }, 130 | "identify": { 131 | "hashes": [ 132 | "sha256:3e8268640da8fa2a62acd5af2c299c45597a9a845d6706b3c19ddd5a46fb32dd", 133 | "sha256:78fcb1bdd37f4ecc954e8cba01680054781412a8aac82aed1998d8b5c8488011" 134 | ], 135 | "version": "==1.4.4" 136 | }, 137 | "importlib-metadata": { 138 | "hashes": [ 139 | "sha256:6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", 140 | "sha256:cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db" 141 | ], 142 | "version": "==0.18" 143 | }, 144 | "more-itertools": { 145 | "hashes": [ 146 | "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", 147 | "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" 148 | ], 149 | "markers": "python_version > '2.7'", 150 | "version": "==7.0.0" 151 | }, 152 | "nodeenv": { 153 | "hashes": [ 154 | "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" 155 | ], 156 | "version": "==1.3.3" 157 | }, 158 | "packaging": { 159 | "hashes": [ 160 | "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", 161 | "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" 162 | ], 163 | "version": "==19.0" 164 | }, 165 | "pluggy": { 166 | "hashes": [ 167 | "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc", 168 | "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c" 169 | ], 170 | "version": "==0.12.0" 171 | }, 172 | "pre-commit": { 173 | "hashes": [ 174 | "sha256:92e406d556190503630fd801958379861c94884693a032ba66629d0351fdccd4", 175 | "sha256:cccc39051bc2457b0c0f7152a411f8e05e3ba2fe1a5613e4ee0833c1c1985ce3" 176 | ], 177 | "index": "pypi", 178 | "version": "==1.17.0" 179 | }, 180 | "py": { 181 | "hashes": [ 182 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", 183 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" 184 | ], 185 | "version": "==1.8.0" 186 | }, 187 | "pydocstyle": { 188 | "hashes": [ 189 | "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", 190 | "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", 191 | "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039" 192 | ], 193 | "index": "pypi", 194 | "version": "==3.0.0" 195 | }, 196 | "pyparsing": { 197 | "hashes": [ 198 | "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", 199 | "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" 200 | ], 201 | "version": "==2.4.0" 202 | }, 203 | "pytest": { 204 | "hashes": [ 205 | "sha256:4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45", 206 | "sha256:926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da" 207 | ], 208 | "index": "pypi", 209 | "version": "==4.6.3" 210 | }, 211 | "pyyaml": { 212 | "hashes": [ 213 | "sha256:57acc1d8533cbe51f6662a55434f0dbecfa2b9eaf115bede8f6fd00115a0c0d3", 214 | "sha256:588c94b3d16b76cfed8e0be54932e5729cc185caffaa5a451e7ad2f7ed8b4043", 215 | "sha256:68c8dd247f29f9a0d09375c9c6b8fdc64b60810ebf07ba4cdd64ceee3a58c7b7", 216 | "sha256:70d9818f1c9cd5c48bb87804f2efc8692f1023dac7f1a1a5c61d454043c1d265", 217 | "sha256:86a93cccd50f8c125286e637328ff4eef108400dd7089b46a7be3445eecfa391", 218 | "sha256:a0f329125a926876f647c9fa0ef32801587a12328b4a3c741270464e3e4fa778", 219 | "sha256:a3c252ab0fa1bb0d5a3f6449a4826732f3eb6c0270925548cac342bc9b22c225", 220 | "sha256:b4bb4d3f5e232425e25dda21c070ce05168a786ac9eda43768ab7f3ac2770955", 221 | "sha256:cd0618c5ba5bda5f4039b9398bb7fb6a317bb8298218c3de25c47c4740e4b95e", 222 | "sha256:ceacb9e5f8474dcf45b940578591c7f3d960e82f926c707788a570b51ba59190", 223 | "sha256:fe6a88094b64132c4bb3b631412e90032e8cfe9745a58370462240b8cb7553cd" 224 | ], 225 | "version": "==5.1.1" 226 | }, 227 | "rope": { 228 | "hashes": [ 229 | "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", 230 | "sha256:c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", 231 | "sha256:f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf" 232 | ], 233 | "index": "pypi", 234 | "version": "==0.14.0" 235 | }, 236 | "six": { 237 | "hashes": [ 238 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 239 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 240 | ], 241 | "version": "==1.12.0" 242 | }, 243 | "snowballstemmer": { 244 | "hashes": [ 245 | "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", 246 | "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" 247 | ], 248 | "version": "==1.2.1" 249 | }, 250 | "toml": { 251 | "hashes": [ 252 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 253 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 254 | ], 255 | "version": "==0.10.0" 256 | }, 257 | "virtualenv": { 258 | "hashes": [ 259 | "sha256:99acaf1e35c7ccf9763db9ba2accbca2f4254d61d1912c5ee364f9cc4a8942a0", 260 | "sha256:fe51cdbf04e5d8152af06c075404745a7419de27495a83f0d72518ad50be3ce8" 261 | ], 262 | "version": "==16.6.0" 263 | }, 264 | "wcwidth": { 265 | "hashes": [ 266 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 267 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 268 | ], 269 | "version": "==0.1.7" 270 | }, 271 | "zipp": { 272 | "hashes": [ 273 | "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d", 274 | "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3" 275 | ], 276 | "version": "==0.5.1" 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /alpaca.py: -------------------------------------------------------------------------------- 1 | """Wraps the HTTP requests for the ASCOM Alpaca API into pythonic classes with methods. 2 | 3 | Attributes: 4 | DEFAULT_API_VERSION (int): Default Alpaca API spec to use if none is specified when 5 | needed. 6 | 7 | """ 8 | 9 | from datetime import datetime 10 | from typing import Optional, Union, List, Dict, Mapping, Any 11 | import dateutil.parser 12 | import requests 13 | 14 | 15 | DEFAULT_API_VERSION = 1 16 | 17 | 18 | class Device: 19 | """Common methods across all ASCOM Alpaca devices. 20 | 21 | Attributes: 22 | address (str): Domain name or IP address of Alpaca server. 23 | Can also specify port number if needed. 24 | device_type (str): One of the recognised ASCOM device types 25 | e.g. telescope (must be lower case). 26 | device_number (int): Zero based device number as set on the server (0 to 27 | 4294967295). 28 | protocall (str): Protocall used to communicate with Alpaca server. 29 | api_version (int): Alpaca API version. 30 | base_url (str): Basic URL to easily append with commands. 31 | 32 | """ 33 | 34 | def __init__( 35 | self, 36 | address: str, 37 | device_type: str, 38 | device_number: int, 39 | protocall: str, 40 | api_version: int, 41 | ): 42 | """Initialize Device object.""" 43 | self.address = address 44 | self.device_type = device_type 45 | self.device_number = device_number 46 | self.api_version = api_version 47 | self.base_url = "%s://%s/api/v%d/%s/%d" % ( 48 | protocall, 49 | address, 50 | api_version, 51 | device_type, 52 | device_number, 53 | ) 54 | 55 | def action(self, Action: str, *Parameters): 56 | """Access functionality beyond the built-in capabilities of the ASCOM device interfaces. 57 | 58 | Args: 59 | Action (str): A well known name that represents the action to be carried out. 60 | *Parameters: List of required parameters or empty if none are required. 61 | 62 | """ 63 | return self._put("action", Action=Action, Parameters=Parameters)["Value"] 64 | 65 | def commandblind(self, Command: str, Raw: bool): 66 | """Transmit an arbitrary string to the device and does not wait for a response. 67 | 68 | Args: 69 | Command (str): The literal command string to be transmitted. 70 | Raw (bool): If true, command is transmitted 'as-is'. 71 | If false, then protocol framing characters may be added prior to 72 | transmission. 73 | 74 | """ 75 | self._put("commandblind", Command=Command, Raw=Raw) 76 | 77 | def commandbool(self, Command: str, Raw: bool): 78 | """Transmit an arbitrary string to the device and wait for a boolean response. 79 | 80 | Args: 81 | Command (str): The literal command string to be transmitted. 82 | Raw (bool): If true, command is transmitted 'as-is'. 83 | If false, then protocol framing characters may be added prior to 84 | transmission. 85 | 86 | """ 87 | return self._put("commandbool", Command=Command, Raw=Raw)["Value"] 88 | 89 | def commandstring(self, Command: str, Raw: bool): 90 | """Transmit an arbitrary string to the device and wait for a string response. 91 | 92 | Args: 93 | Command (str): The literal command string to be transmitted. 94 | Raw (bool): If true, command is transmitted 'as-is'. 95 | If false, then protocol framing characters may be added prior to 96 | transmission. 97 | 98 | """ 99 | return self._put("commandstring", Command=Command, Raw=Raw)["Value"] 100 | 101 | def connected(self, Connected: Optional[bool] = None): 102 | """Retrieve or set the connected state of the device. 103 | 104 | Args: 105 | Connected (bool): Set True to connect to device hardware. 106 | Set False to disconnect from device hardware. 107 | Set None to get connected state (default). 108 | 109 | """ 110 | if Connected == None: 111 | return self._get("connected") 112 | self._put("connected", Connected=Connected) 113 | 114 | def description(self) -> str: 115 | """Get description of the device.""" 116 | return self._get("name") 117 | 118 | def driverinfo(self) -> List[str]: 119 | """Get information of the device.""" 120 | return [i.strip() for i in self._get("driverinfo").split(",")] 121 | 122 | def driverversion(self) -> str: 123 | """Get string containing only the major and minor version of the driver.""" 124 | return self._get("driverversion") 125 | 126 | def interfaceversion(self) -> int: 127 | """ASCOM Device interface version number that this device supports.""" 128 | return self._get("interfaceversion") 129 | 130 | def name(self) -> str: 131 | """Get name of the device.""" 132 | return self._get("name") 133 | 134 | def supportedactions(self) -> List[str]: 135 | """Get list of action names supported by this driver.""" 136 | return self._get("supportedactions") 137 | 138 | def _get(self, attribute: str, **data): 139 | """Send an HTTP GET request to an Alpaca server and check response for errors. 140 | 141 | Args: 142 | attribute (str): Attribute to get from server. 143 | **data: Data to send with request. 144 | 145 | """ 146 | response = requests.get("%s/%s" % (self.base_url, attribute), data=data) 147 | self.__check_error(response) 148 | return response.json()["Value"] 149 | 150 | def _put(self, attribute: str, **data): 151 | """Send an HTTP PUT request to an Alpaca server and check response for errors. 152 | 153 | Args: 154 | attribute (str): Attribute to put to server. 155 | **data: Data to send with request. 156 | 157 | """ 158 | response = requests.put("%s/%s" % (self.base_url, attribute), data=data) 159 | self.__check_error(response) 160 | return response.json() 161 | 162 | def __check_error(self, response: requests.Response): 163 | """Check response from Alpaca server for Errors. 164 | 165 | Args: 166 | response (Response): Response from Alpaca server to check. 167 | 168 | """ 169 | j = response.json() 170 | if j["ErrorNumber"] != 0: 171 | raise NumericError(j["ErrorNumber"], j["ErrorMessage"]) 172 | elif response.status_code == 400 or response.status_code == 500: 173 | raise ErrorMessage(j["Value"]) 174 | 175 | 176 | class Switch(Device): 177 | """Switch specific methods.""" 178 | 179 | def __init__( 180 | self, 181 | address: str, 182 | device_number: int, 183 | protocall: str = "http", 184 | api_version: int = DEFAULT_API_VERSION, 185 | ): 186 | """Initialize Switch object.""" 187 | super().__init__(address, "switch", device_number, protocall, api_version) 188 | 189 | def maxswitch(self) -> int: 190 | """Count of switch devices managed by this driver. 191 | 192 | Returns: 193 | Number of switch devices managed by this driver. Devices are numbered from 0 194 | to MaxSwitch - 1. 195 | 196 | """ 197 | return self._get("maxswitch") 198 | 199 | def canwrite(self, Id: Optional[int] = 0) -> bool: 200 | """Indicate whether the specified switch device can be written to. 201 | 202 | Notes: 203 | Devices are numbered from 0 to MaxSwitch - 1. 204 | 205 | Args: 206 | Id (int): The device number. 207 | 208 | Returns: 209 | Whether the specified switch device can be written to, default true. This is 210 | false if the device cannot be written to, for example a limit switch or a 211 | sensor. 212 | 213 | """ 214 | return self._get("canwrite", Id=Id) 215 | 216 | def getswitch(self, Id: Optional[int] = 0) -> bool: 217 | """Return the state of switch device id as a boolean. 218 | 219 | Notes: 220 | Devices are numbered from 0 to MaxSwitch - 1. 221 | 222 | Args: 223 | Id (int): The device number. 224 | 225 | Returns: 226 | State of switch device id as a boolean. 227 | 228 | """ 229 | return self._get("getswitch", Id=Id) 230 | 231 | def getswitchdescription(self, Id: Optional[int] = 0) -> str: 232 | """Get the description of the specified switch device. 233 | 234 | Notes: 235 | Devices are numbered from 0 to MaxSwitch - 1. 236 | 237 | Args: 238 | Id (int): The device number. 239 | 240 | Returns: 241 | Description of the specified switch device. 242 | 243 | """ 244 | return self._get("getswitchdescription", Id=Id) 245 | 246 | def getswitchname(self, Id: Optional[int] = 0) -> str: 247 | """Get the name of the specified switch device. 248 | 249 | Notes: 250 | Devices are numbered from 0 to MaxSwitch - 1. 251 | 252 | Args: 253 | Id (int): The device number. 254 | 255 | Returns: 256 | Name of the specified switch device. 257 | 258 | """ 259 | return self._get("getswitchname", Id=Id) 260 | 261 | def getswitchvalue(self, Id: Optional[int] = 0) -> str: 262 | """Get the value of the specified switch device as a double. 263 | 264 | Notes: 265 | Devices are numbered from 0 to MaxSwitch - 1. 266 | 267 | Args: 268 | Id (int): The device number. 269 | 270 | Returns: 271 | Value of the specified switch device. 272 | 273 | """ 274 | return self._get("getswitchvalue", Id=Id) 275 | 276 | def minswitchvalue(self, Id: Optional[int] = 0) -> str: 277 | """Get the minimum value of the specified switch device as a double. 278 | 279 | Notes: 280 | Devices are numbered from 0 to MaxSwitch - 1. 281 | 282 | Args: 283 | Id (int): The device number. 284 | 285 | Returns: 286 | Minimum value of the specified switch device as a double. 287 | 288 | """ 289 | return self._get("minswitchvalue", Id=Id) 290 | 291 | def setswitch(self, Id: int, State: bool): 292 | """Set a switch controller device to the specified state, True or False. 293 | 294 | Notes: 295 | Devices are numbered from 0 to MaxSwitch - 1. 296 | 297 | Args: 298 | Id (int): The device number. 299 | State (bool): The required control state (True or False). 300 | 301 | """ 302 | self._put("setswitch", Id=Id, State=State) 303 | 304 | def setswitchname(self, Id: int, Name: str): 305 | """Set a switch device name to the specified value. 306 | 307 | Notes: 308 | Devices are numbered from 0 to MaxSwitch - 1. 309 | 310 | Args: 311 | Id (int): The device number. 312 | Name (str): The name of the device. 313 | 314 | """ 315 | self._put("setswitchname", Id=Id, Name=Name) 316 | 317 | def setswitchvalue(self, Id: int, Value: float): 318 | """Set a switch device value to the specified value. 319 | 320 | Notes: 321 | Devices are numbered from 0 to MaxSwitch - 1. 322 | 323 | Args: 324 | Id (int): The device number. 325 | Value (float): Value to be set, between MinSwitchValue and MaxSwitchValue. 326 | 327 | """ 328 | self._put("setswitchvalue", Id=Id, Value=Value) 329 | 330 | def switchstep(self, Id: Optional[int] = 0) -> str: 331 | """Return the step size that this device supports. 332 | 333 | Return the step size that this device supports (the difference between 334 | successive values of the device). 335 | 336 | Notes: 337 | Devices are numbered from 0 to MaxSwitch - 1. 338 | 339 | Args: 340 | Id (int): The device number. 341 | 342 | Returns: 343 | Maximum value of the specified switch device as a double. 344 | 345 | """ 346 | return self._get("switchstep", Id=Id) 347 | 348 | 349 | class SafetyMonitor(Device): 350 | """Safety monitor specific methods.""" 351 | 352 | def __init__( 353 | self, 354 | address: str, 355 | device_number: int, 356 | protocall: str = "http", 357 | api_version: int = DEFAULT_API_VERSION, 358 | ): 359 | """Initialize SafetyMonitor object.""" 360 | super().__init__( 361 | address, "safetymonitor", device_number, protocall, api_version 362 | ) 363 | 364 | def issafe(self) -> bool: 365 | """Indicate whether the monitored state is safe for use. 366 | 367 | Returns: 368 | True if the state is safe, False if it is unsafe. 369 | 370 | """ 371 | return self._get("issafe") 372 | 373 | 374 | class Dome(Device): 375 | """Dome specific methods.""" 376 | 377 | def __init__( 378 | self, 379 | address: str, 380 | device_number: int, 381 | protocall: str = "http", 382 | api_version: int = DEFAULT_API_VERSION, 383 | ): 384 | """Initialize Dome object.""" 385 | super().__init__(address, "dome", device_number, protocall, api_version) 386 | 387 | def altitude(self) -> float: 388 | """Dome altitude. 389 | 390 | Returns: 391 | Dome altitude (degrees, horizon zero and increasing positive to 90 zenith). 392 | 393 | """ 394 | return self._get("altitude") 395 | 396 | def athome(self) -> bool: 397 | """Indicate whether the dome is in the home position. 398 | 399 | Notes: 400 | This is normally used following a findhome() operation. The value is reset 401 | with any azimuth slew operation that moves the dome away from the home 402 | position. athome() may also become true durng normal slew operations, if the 403 | dome passes through the home position and the dome controller hardware is 404 | capable of detecting that; or at the end of a slew operation if the dome 405 | comes to rest at the home position. 406 | 407 | Returns: 408 | True if dome is in the home position. 409 | 410 | """ 411 | return self._get("athome") 412 | 413 | def atpark(self) -> bool: 414 | """Indicate whether the telescope is at the park position. 415 | 416 | Notes: 417 | Set only following a park() operation and reset with any slew operation. 418 | 419 | Returns: 420 | True if the dome is in the programmed park position. 421 | 422 | """ 423 | return self._get("atpark") 424 | 425 | def azimuth(self) -> float: 426 | """Dome azimuth. 427 | 428 | Returns: 429 | Dome azimuth (degrees, North zero and increasing clockwise, i.e., 90 East, 430 | 180 South, 270 West). 431 | 432 | """ 433 | return self._get("azimuth") 434 | 435 | def canfindhome(self) -> bool: 436 | """Indicate whether the dome can find the home position. 437 | 438 | Returns: 439 | True if the dome can move to the home position. 440 | 441 | """ 442 | return self._get("canfindhome") 443 | 444 | def canpark(self) -> bool: 445 | """Indicate whether the dome can be parked. 446 | 447 | Returns: 448 | True if the dome is capable of programmed parking (park() method). 449 | 450 | """ 451 | return self._get("canpark") 452 | 453 | def cansetaltitude(self) -> bool: 454 | """Indicate whether the dome altitude can be set. 455 | 456 | Returns: 457 | True if driver is capable of setting the dome altitude. 458 | 459 | """ 460 | return self._get("cansetaltitude") 461 | 462 | def cansetazimuth(self) -> bool: 463 | """Indicate whether the dome azimuth can be set. 464 | 465 | Returns: 466 | True if driver is capable of setting the dome azimuth. 467 | 468 | """ 469 | return self._get("cansetazimuth") 470 | 471 | def cansetpark(self) -> bool: 472 | """Indicate whether the dome park position can be set. 473 | 474 | Returns: 475 | True if driver is capable of setting the dome park position. 476 | 477 | """ 478 | return self._get("cansetpark") 479 | 480 | def cansetshutter(self) -> bool: 481 | """Indicate whether the dome shutter can be opened. 482 | 483 | Returns: 484 | True if driver is capable of automatically operating shutter. 485 | 486 | """ 487 | return self._get("cansetshutter") 488 | 489 | def canslave(self) -> bool: 490 | """Indicate whether the dome supports slaving to a telescope. 491 | 492 | Returns: 493 | True if driver is capable of slaving to a telescope. 494 | 495 | """ 496 | return self._get("canslave") 497 | 498 | def cansyncazimuth(self) -> bool: 499 | """Indicate whether the dome azimuth position can be synched. 500 | 501 | Notes: 502 | True if driver is capable of synchronizing the dome azimuth position using 503 | the synctoazimuth(float) method. 504 | 505 | Returns: 506 | True or False value. 507 | 508 | """ 509 | return self._get("cansyncazimuth") 510 | 511 | def shutterstatus(self) -> int: 512 | """Status of the dome shutter or roll-off roof. 513 | 514 | Notes: 515 | 0 = Open, 1 = Closed, 2 = Opening, 3 = Closing, 4 = Shutter status error. 516 | 517 | Returns: 518 | Status of the dome shutter or roll-off roof. 519 | 520 | """ 521 | return self._get("shutterstatus") 522 | 523 | def slaved(self, Slaved: Optional[bool] = None) -> bool: 524 | """Set or indicate whether the dome is slaved to the telescope. 525 | 526 | Returns: 527 | True or False value in not set. 528 | 529 | """ 530 | if Slaved == None: 531 | return self._get("slaved") 532 | self._put("slaved", Slaved=Slaved) 533 | 534 | def slewing(self) -> bool: 535 | """Indicate whether the any part of the dome is moving. 536 | 537 | Notes: 538 | True if any part of the dome is currently moving, False if all dome 539 | components are steady. 540 | 541 | Return: 542 | True or False value. 543 | 544 | """ 545 | return self._get("slewing") 546 | 547 | def abortslew(self): 548 | """Immediately cancel current dome operation. 549 | 550 | Notes: 551 | Calling this method will immediately disable hardware slewing (Slaved will 552 | become False). 553 | 554 | """ 555 | self._put("abortslew") 556 | 557 | def closeshutter(self): 558 | """Close the shutter or otherwise shield telescope from the sky.""" 559 | self._put("closeshutter") 560 | 561 | def findhome(self): 562 | """Start operation to search for the dome home position. 563 | 564 | Notes: 565 | After home position is established initializes azimuth to the default value 566 | and sets the athome flag. 567 | 568 | """ 569 | self._put("findhome") 570 | 571 | def openshutter(self): 572 | """Open shutter or otherwise expose telescope to the sky.""" 573 | self._put("openshutter") 574 | 575 | def park(self): 576 | """Rotate dome in azimuth to park position. 577 | 578 | Notes: 579 | After assuming programmed park position, sets atpark flag. 580 | 581 | """ 582 | self._put("park") 583 | 584 | def setpark(self): 585 | """Set current azimuth, altitude position of dome to be the park position.""" 586 | self._put("setpark") 587 | 588 | def slewtoaltitude(self, Altitude: float): 589 | """Slew the dome to the given altitude position.""" 590 | self._put("slewtoaltitude", Altitude=Altitude) 591 | 592 | def slewtoazimuth(self, Azimuth: float): 593 | """Slew the dome to the given azimuth position. 594 | 595 | Args: 596 | Azimuth (float): Target dome azimuth (degrees, North zero and increasing 597 | clockwise. i.e., 90 East, 180 South, 270 West). 598 | 599 | """ 600 | self._put("slewtoazimuth", Azimuth=Azimuth) 601 | 602 | def synctoazimuth(self, Azimuth: float): 603 | """Synchronize the current position of the dome to the given azimuth. 604 | 605 | Args: 606 | Azimuth (float): Target dome azimuth (degrees, North zero and increasing 607 | clockwise. i.e., 90 East, 180 South, 270 West). 608 | 609 | """ 610 | self._put("synctoazimuth", Azimuth=Azimuth) 611 | 612 | 613 | class Camera(Device): 614 | """Camera specific methods.""" 615 | 616 | def __init__( 617 | self, 618 | address: str, 619 | device_number: int, 620 | protocall: str = "http", 621 | api_version: int = DEFAULT_API_VERSION, 622 | ): 623 | """Initialize Camera object.""" 624 | super().__init__(address, "camera", device_number, protocall, api_version) 625 | 626 | def bayeroffsetx(self) -> int: 627 | """Return the X offset of the Bayer matrix, as defined in SensorType.""" 628 | return self._get("bayeroffsetx") 629 | 630 | def bayeroffsety(self) -> int: 631 | """Return the Y offset of the Bayer matrix, as defined in SensorType.""" 632 | return self._get("bayeroffsety") 633 | 634 | def binx(self, BinX: Optional[int] = None) -> int: 635 | """Set or return the binning factor for the X axis. 636 | 637 | Args: 638 | BinX (int): The X binning value. 639 | 640 | Returns: 641 | Binning factor for the X axis. 642 | 643 | """ 644 | if BinX == None: 645 | return self._get("binx") 646 | self._put("binx", BinX=BinX) 647 | 648 | def biny(self, BinY: Optional[int] = None) -> int: 649 | """Set or return the binning factor for the Y axis. 650 | 651 | Args: 652 | BinY (int): The Y binning value. 653 | 654 | Returns: 655 | Binning factor for the Y axis. 656 | 657 | """ 658 | if BinY == None: 659 | return self._get("biny") 660 | self._put("biny", BinY=BinY) 661 | 662 | def camerastate(self) -> int: 663 | """Return the camera operational state. 664 | 665 | Notes: 666 | 0 = CameraIdle, 1 = CameraWaiting, 2 = CameraExposing, 667 | 3 = CameraReading, 4 = CameraDownload, 5 = CameraError. 668 | 669 | Returns: 670 | Current camera operational state as an integer. 671 | 672 | """ 673 | return self._get("camerastate") 674 | 675 | def cameraxsize(self) -> int: 676 | """Return the width of the CCD camera chip.""" 677 | return self._get("cameraxsize") 678 | 679 | def cameraysize(self) -> int: 680 | """Return the height of the CCD camera chip.""" 681 | return self._get("cameraysize") 682 | 683 | def canabortexposure(self) -> bool: 684 | """Indicate whether the camera can abort exposures.""" 685 | return self._get("canabortexposure") 686 | 687 | def canasymmetricbin(self) -> bool: 688 | """Indicate whether the camera supports asymmetric binning.""" 689 | return self._get("canasymmetricbin") 690 | 691 | def canfastreadout(self) -> bool: 692 | """Indicate whether the camera has a fast readout mode.""" 693 | return self._get("canfastreadout") 694 | 695 | def cangetcoolerpower(self) -> bool: 696 | """Indicate whether the camera's cooler power setting can be read.""" 697 | return self._get("cangetcoolerpower") 698 | 699 | def canpulseguide(self) -> bool: 700 | """Indicate whether this camera supports pulse guiding.""" 701 | return self._get("canpulseguide") 702 | 703 | def cansetccdtemperature(self) -> bool: 704 | """Indicate whether this camera supports setting the CCD temperature.""" 705 | return self._get("cansetccdtemperature") 706 | 707 | def canstopexposure(self) -> bool: 708 | """Indicate whether this camera can stop an exposure that is in progress.""" 709 | return self._get("canstopexposure") 710 | 711 | def ccdtemperature(self) -> float: 712 | """Return the current CCD temperature in degrees Celsius.""" 713 | return self._get("ccdtemperature") 714 | 715 | def cooleron(self, CoolerOn: Optional[bool] = None) -> bool: 716 | """Turn the camera cooler on and off or return the current cooler on/off state. 717 | 718 | Notes: 719 | True = cooler on, False = cooler off. 720 | 721 | Args: 722 | CoolerOn (bool): Cooler state. 723 | 724 | Returns: 725 | Current cooler on/off state. 726 | 727 | """ 728 | if CoolerOn == None: 729 | return self._get("cooleron") 730 | self._put("cooleron", CoolerOn=CoolerOn) 731 | 732 | def coolerpower(self) -> float: 733 | """Return the present cooler power level, in percent.""" 734 | return self._get("coolerpower") 735 | 736 | def electronsperadu(self) -> float: 737 | """Return the gain of the camera in photoelectrons per A/D unit.""" 738 | return self._get("electronsperadu") 739 | 740 | def exposuremax(self) -> float: 741 | """Return the maximum exposure time supported by StartExposure.""" 742 | return self._get("exposuremax") 743 | 744 | def exposuremin(self) -> float: 745 | """Return the minimum exposure time supported by StartExposure.""" 746 | return self._get("exposuremin") 747 | 748 | def exposureresolution(self) -> float: 749 | """Return the smallest increment in exposure time supported by StartExposure.""" 750 | return self._get("exposureresolution") 751 | 752 | def fastreadout(self, FastReadout: Optional[bool] = None) -> bool: 753 | """Set or return whether Fast Readout Mode is enabled. 754 | 755 | Args: 756 | FastReadout (bool): True to enable fast readout mode. 757 | 758 | Returns: 759 | Whether Fast Readout Mode is enabled. 760 | 761 | """ 762 | if FastReadout == None: 763 | return self._get("fastreadout") 764 | self._put("fastreadout", FastReadout=FastReadout) 765 | 766 | def fullwellcapacity(self) -> float: 767 | """Report the full well capacity of the camera. 768 | 769 | Report the full well capacity of the camera in electrons, at the current 770 | camera settings (binning, SetupDialog settings, etc.). 771 | 772 | Returns: 773 | Full well capacity of the camera. 774 | 775 | """ 776 | return self._get("fullwellcapacity") 777 | 778 | def gain(self, Gain: Optional[int] = None) -> int: 779 | """Set or return an index into the Gains array. 780 | 781 | Args: 782 | Gain (int): Index of the current camera gain in the Gains string array. 783 | 784 | Returns: 785 | Index into the Gains array for the selected camera gain. 786 | 787 | """ 788 | if Gain == None: 789 | return self._get("gain") 790 | self._put("gain", Gain=Gain) 791 | 792 | def gainmax(self) -> int: 793 | """Maximum value of Gain.""" 794 | return self._get("gainmax") 795 | 796 | def gainmin(self) -> int: 797 | """Minimum value of Gain.""" 798 | return self._get("gainmin") 799 | 800 | def gains(self) -> List[int]: 801 | """Gains supported by the camera.""" 802 | return self._get("gains") 803 | 804 | def hasshutter(self) -> bool: 805 | """Indicate whether the camera has a mechanical shutter.""" 806 | return self._get("hasshutter") 807 | 808 | def heatsinktemperature(self) -> float: 809 | """Return the current heat sink temperature. 810 | 811 | Returns: 812 | Current heat sink temperature (called "ambient temperature" by some 813 | manufacturers) in degrees Celsius. 814 | 815 | """ 816 | return self._get("heatsinktemperature") 817 | 818 | def imagearray(self) -> List[int]: 819 | r"""Return an array of integers containing the exposure pixel values. 820 | 821 | Return an array of 32bit integers containing the pixel values from the last 822 | exposure. This call can return either a 2 dimension (monochrome images) or 3 823 | dimension (colour or multi-plane images) array of size NumX * NumY or NumX * 824 | NumY * NumPlanes. Where applicable, the size of NumPlanes has to be determined 825 | by inspection of the returned Array. Since 32bit integers are always returned 826 | by this call, the returned JSON Type value (0 = Unknown, 1 = short(16bit), 827 | 2 = int(32bit), 3 = Double) is always 2. The number of planes is given in the 828 | returned Rank value. When de-serialising to an object it helps enormously to 829 | know the array Rank beforehand so that the correct data class can be used. This 830 | can be achieved through a regular expression or by direct parsing of the 831 | returned JSON string to extract the Type and Rank values before de-serialising. 832 | This regular expression accomplishes the extraction into two named groups Type 833 | and Rank ^*"Type":(?\d*),"Rank":(?\d*) which can then be used to 834 | select the correct de-serialisation data class. 835 | 836 | Returns: 837 | Array of integers containing the exposure pixel values. 838 | 839 | """ 840 | return self._get("imagearray") 841 | 842 | def imagearrayvariant(self) -> List[int]: 843 | r"""Return an array of integers containing the exposure pixel values. 844 | 845 | Return an array of 32bit integers containing the pixel values from the last 846 | exposure. This call can return either a 2 dimension (monochrome images) or 3 847 | dimension (colour or multi-plane images) array of size NumX * NumY or NumX * 848 | NumY * NumPlanes. Where applicable, the size of NumPlanes has to be determined 849 | by inspection of the returned Array. Since 32bit integers are always returned 850 | by this call, the returned JSON Type value (0 = Unknown, 1 = short(16bit), 851 | 2 = int(32bit), 3 = Double) is always 2. The number of planes is given in the 852 | returned Rank value. When de-serialising to an object it helps enormously to 853 | know the array Rank beforehand so that the correct data class can be used. This 854 | can be achieved through a regular expression or by direct parsing of the 855 | returned JSON string to extract the Type and Rank values before de-serialising. 856 | This regular expression accomplishes the extraction into two named groups Type 857 | and Rank ^*"Type":(?\d*),"Rank":(?\d*) which can then be used to 858 | select the correct de-serialisation data class. 859 | 860 | Returns: 861 | Array of integers containing the exposure pixel values. 862 | 863 | """ 864 | return self._get("imagearrayvariant") 865 | 866 | def imageready(self) -> bool: 867 | """Indicate that an image is ready to be downloaded.""" 868 | return self._get("imageready") 869 | 870 | def ispulseguiding(self) -> bool: 871 | """Indicatee that the camera is pulse guideing.""" 872 | return self._get("ispulseguiding") 873 | 874 | def lastexposureduration(self) -> float: 875 | """Report the actual exposure duration in seconds (i.e. shutter open time).""" 876 | return self._get("lastexposureduration") 877 | 878 | def lastexposurestarttime(self) -> str: 879 | """Start time of the last exposure in FITS standard format. 880 | 881 | Reports the actual exposure start in the FITS-standard 882 | CCYY-MM-DDThh:mm:ss[.sss...] format. 883 | 884 | Returns: 885 | Start time of the last exposure in FITS standard format. 886 | 887 | """ 888 | return self._get("lastexposurestarttime") 889 | 890 | def maxadu(self) -> int: 891 | """Camera's maximum ADU value.""" 892 | return self._get("maxadu") 893 | 894 | def maxbinx(self) -> int: 895 | """Maximum binning for the camera X axis.""" 896 | return self._get("maxbinx") 897 | 898 | def maxbiny(self) -> int: 899 | """Maximum binning for the camera Y axis.""" 900 | return self._get("maxbiny") 901 | 902 | def numx(self, NumX: Optional[int] = None) -> int: 903 | """Set or return the current subframe width. 904 | 905 | Args: 906 | NumX (int): Subframe width, if binning is active, value is in binned 907 | pixels. 908 | 909 | Returns: 910 | Current subframe width. 911 | 912 | """ 913 | if NumX == None: 914 | return self._get("numx") 915 | self._put("numx", NumX=NumX) 916 | 917 | def numy(self, NumY: Optional[int] = None) -> int: 918 | """Set or return the current subframe height. 919 | 920 | Args: 921 | NumX (int): Subframe height, if binning is active, value is in binned 922 | pixels. 923 | 924 | Returns: 925 | Current subframe height. 926 | 927 | """ 928 | if NumY == None: 929 | return self._get("numy") 930 | self._put("numy", NumY=NumY) 931 | 932 | def percentcompleted(self) -> int: 933 | """Indicate percentage completeness of the current operation. 934 | 935 | Returns: 936 | If valid, returns an integer between 0 and 100, where 0 indicates 0% 937 | progress (function just started) and 100 indicates 100% progress (i.e. 938 | completion). 939 | 940 | """ 941 | return self._get("percentcompleted") 942 | 943 | def pixelsizex(self): 944 | """Width of CCD chip pixels (microns).""" 945 | return self._get("pixelsizex") 946 | 947 | def pixelsizey(self): 948 | """Height of CCD chip pixels (microns).""" 949 | return self._get("pixelsizey") 950 | 951 | def readoutmode(self, ReadoutMode: Optional[int] = None) -> int: 952 | """Indicate the canera's readout mode as an index into the array ReadoutModes.""" 953 | if ReadoutMode == None: 954 | return self._get("readoutmode") 955 | self._put("readoutmode", ReadoutMode=ReadoutMode) 956 | 957 | def readoutmodes(self) -> List[int]: 958 | """List of available readout modes.""" 959 | return self._get("readoutmodes") 960 | 961 | def sensorname(self) -> str: 962 | """Name of the sensor used within the camera.""" 963 | return self._get("sensorname") 964 | 965 | def sensortype(self) -> int: 966 | """Type of information returned by the the camera sensor (monochrome or colour). 967 | 968 | Notes: 969 | 0 = Monochrome, 1 = Colour not requiring Bayer decoding, 2 = RGGB Bayer 970 | encoding, 3 = CMYG Bayer encoding, 4 = CMYG2 Bayer encoding, 5 = LRGB 971 | TRUESENSE Bayer encoding. 972 | 973 | Returns: 974 | Value indicating whether the sensor is monochrome, or what Bayer matrix it 975 | encodes. 976 | 977 | """ 978 | return self._get("sensortype") 979 | 980 | def setccdtemperature(self, SetCCDTemperature: Optional[float] = None) -> float: 981 | """Set or return the camera's cooler setpoint (degrees Celsius). 982 | 983 | Args: 984 | SetCCDTemperature (float): Temperature set point (degrees Celsius). 985 | 986 | Returns: 987 | Camera's cooler setpoint (degrees Celsius). 988 | 989 | """ 990 | if SetCCDTemperature == None: 991 | return self._get("setccdtemperature") 992 | self._put("setccdtemperature", SetCCDTemperature=SetCCDTemperature) 993 | 994 | def startx(self, StartX: Optional[int] = None) -> int: 995 | """Set or return the current subframe X axis start position. 996 | 997 | Args: 998 | StartX (int): The subframe X axis start position in binned pixels. 999 | 1000 | Returns: 1001 | Sets the subframe start position for the X axis (0 based) and returns the 1002 | current value. If binning is active, value is in binned pixels. 1003 | 1004 | """ 1005 | if StartX == None: 1006 | return self._get("startx") 1007 | self._put("startx", StartX=StartX) 1008 | 1009 | def starty(self, StartY: Optional[int] = None) -> int: 1010 | """Set or return the current subframe Y axis start position. 1011 | 1012 | Args: 1013 | StartY (int): The subframe Y axis start position in binned pixels. 1014 | 1015 | Returns: 1016 | Sets the subframe start position for the Y axis (0 based) and returns the 1017 | current value. If binning is active, value is in binned pixels. 1018 | 1019 | """ 1020 | if StartY == None: 1021 | return self._get("starty") 1022 | self._put("starty", StartY=StartY) 1023 | 1024 | def abortexposure(self): 1025 | """Abort the current exposure, if any, and returns the camera to Idle state.""" 1026 | self._put("abortexposure") 1027 | 1028 | def pulseguide(self, Direction: int, Duration: int): 1029 | """Pulse guide in the specified direction for the specified time. 1030 | 1031 | Args: 1032 | Direction (int): Direction of movement (0 = North, 1 = South, 2 = East, 1033 | 3 = West). 1034 | Duration (int): Duration of movement in milli-seconds. 1035 | 1036 | """ 1037 | self._put("pulseguide", Direction=Direction, Duration=Duration) 1038 | 1039 | def startexposure(self, Duration: float, Light: bool): 1040 | """Start an exposure. 1041 | 1042 | Notes: 1043 | Use ImageReady to check when the exposure is complete. 1044 | 1045 | Args: 1046 | Duration (float): Duration of exposure in seconds. 1047 | Light (bool): True if light frame, false if dark frame. 1048 | 1049 | """ 1050 | self._put("startexposure", Duration=Duration, Light=Light) 1051 | 1052 | def stopexposure(self): 1053 | """Stop the current exposure, if any. 1054 | 1055 | Notes: 1056 | If an exposure is in progress, the readout process is initiated. Ignored if 1057 | readout is already in process. 1058 | 1059 | """ 1060 | self._put("stopexposure") 1061 | 1062 | 1063 | class FilterWheel(Device): 1064 | """Filter wheel specific methods.""" 1065 | 1066 | def __init__( 1067 | self, 1068 | address: str, 1069 | device_number: int, 1070 | protocall: str = "http", 1071 | api_version: int = DEFAULT_API_VERSION, 1072 | ): 1073 | """Initialize FilterWheel object.""" 1074 | super().__init__(address, "filterwheel", device_number, protocall, api_version) 1075 | 1076 | def focusoffsets(self) -> List[int]: 1077 | """Filter focus offsets. 1078 | 1079 | Returns: 1080 | An integer array of filter focus offsets. 1081 | 1082 | """ 1083 | return self._get("focusoffsets") 1084 | 1085 | def names(self) -> List[str]: 1086 | """Filter wheel filter names. 1087 | 1088 | Returns: 1089 | Names of the filters. 1090 | 1091 | """ 1092 | return self._get("names") 1093 | 1094 | def position(self, Position: Optional[int] = None): 1095 | """Set or return the filter wheel position. 1096 | 1097 | Args: 1098 | Position (int): Number of the filter wheel position to select. 1099 | 1100 | Returns: 1101 | Returns the current filter wheel position. 1102 | 1103 | """ 1104 | if Position == None: 1105 | return self._get("position") 1106 | self._put("position", Position=Position) 1107 | 1108 | 1109 | class Telescope(Device): 1110 | """Telescope specific methods.""" 1111 | 1112 | def __init__( 1113 | self, 1114 | address: str, 1115 | device_number: int, 1116 | protocall: str = "http", 1117 | api_version: int = DEFAULT_API_VERSION, 1118 | ): 1119 | """Initialize Telescope object.""" 1120 | super().__init__(address, "telescope", device_number, protocall, api_version) 1121 | 1122 | def alignmentmode(self): 1123 | """Return the current mount alignment mode. 1124 | 1125 | Returns: 1126 | Alignment mode of the mount (Alt/Az, Polar, German Polar). 1127 | 1128 | """ 1129 | return self._get("alignmentmode") 1130 | 1131 | def altitude(self): 1132 | """Return the mount's Altitude above the horizon. 1133 | 1134 | Returns: 1135 | Altitude of the telescope's current position (degrees, positive up). 1136 | 1137 | """ 1138 | return self._get("altitude") 1139 | 1140 | def aperturearea(self): 1141 | """Return the telescope's aperture. 1142 | 1143 | Returns: 1144 | Area of the telescope's aperture (square meters). 1145 | 1146 | """ 1147 | return self._get("aperturearea") 1148 | 1149 | def aperturediameter(self): 1150 | """Return the telescope's effective aperture. 1151 | 1152 | Returns: 1153 | Telescope's effective aperture diameter (meters). 1154 | 1155 | """ 1156 | return self._get("aperturediameter") 1157 | 1158 | def athome(self): 1159 | """Indicate whether the mount is at the home position. 1160 | 1161 | Returns: 1162 | True if the mount is stopped in the Home position. Must be False if the 1163 | telescope does not support homing. 1164 | 1165 | """ 1166 | return self._get("athome") 1167 | 1168 | def atpark(self): 1169 | """Indicate whether the telescope is at the park position. 1170 | 1171 | Returns: 1172 | True if the telescope has been put into the parked state by the seee park() 1173 | method. Set False by calling the unpark() method. 1174 | 1175 | """ 1176 | return self._get("atpark") 1177 | 1178 | def azimuth(self): 1179 | """Return the telescope's aperture. 1180 | 1181 | Return: 1182 | Azimuth of the telescope's current position (degrees, North-referenced, 1183 | positive East/clockwise). 1184 | 1185 | """ 1186 | return self._get("azimuth") 1187 | 1188 | def canfindhome(self): 1189 | """Indicate whether the mount can find the home position. 1190 | 1191 | Returns: 1192 | True if this telescope is capable of programmed finding its home position. 1193 | 1194 | """ 1195 | return self._get("canfindhome") 1196 | 1197 | def canpark(self): 1198 | """Indicate whether the telescope can be parked. 1199 | 1200 | Returns: 1201 | True if this telescope is capable of programmed parking. 1202 | 1203 | """ 1204 | return self._get("canpark") 1205 | 1206 | def canpulseguide(self): 1207 | """Indicate whether the telescope can be pulse guided. 1208 | 1209 | Returns: 1210 | True if this telescope is capable of software-pulsed guiding (via the 1211 | pulseguide(int, int) method). 1212 | 1213 | """ 1214 | return self._get("canpulseguide") 1215 | 1216 | def cansetdeclinationrate(self): 1217 | """Indicate whether the DeclinationRate property can be changed. 1218 | 1219 | Returns: 1220 | True if the DeclinationRate property can be changed to provide offset 1221 | tracking in the declination axis. 1222 | 1223 | """ 1224 | return self._get("cansetdeclinationrate") 1225 | 1226 | def cansetguiderates(self): 1227 | """Indicate whether the DeclinationRate property can be changed. 1228 | 1229 | Returns: 1230 | True if the guide rate properties used for pulseguide(int, int) can ba 1231 | adjusted. 1232 | 1233 | """ 1234 | return self._get("cansetguiderates") 1235 | 1236 | def cansetpark(self): 1237 | """Indicate whether the telescope park position can be set. 1238 | 1239 | Returns: 1240 | True if this telescope is capable of programmed setting of its park position 1241 | (setpark() method). 1242 | 1243 | """ 1244 | return self._get("cansetpark") 1245 | 1246 | def cansetpierside(self): 1247 | """Indicate whether the telescope SideOfPier can be set. 1248 | 1249 | Returns: 1250 | True if the SideOfPier property can be set, meaning that the mount can be 1251 | forced to flip. 1252 | 1253 | """ 1254 | return self._get("cansetpierside") 1255 | 1256 | def cansetrightascensionrate(self): 1257 | """Indicate whether the RightAscensionRate property can be changed. 1258 | 1259 | Returns: 1260 | True if the RightAscensionRate property can be changed to provide offset 1261 | tracking in the right ascension axis. 1262 | 1263 | """ 1264 | return self._get("cansetrightascensionrate") 1265 | 1266 | def cansettracking(self): 1267 | """Indicate whether the Tracking property can be changed. 1268 | 1269 | Returns: 1270 | True if the Tracking property can be changed, turning telescope sidereal 1271 | tracking on and off. 1272 | 1273 | """ 1274 | return self._get("cansettracking") 1275 | 1276 | def canslew(self): 1277 | """Indicate whether the telescope can slew synchronously. 1278 | 1279 | Returns: 1280 | True if this telescope is capable of programmed slewing (synchronous or 1281 | asynchronous) to equatorial coordinates. 1282 | 1283 | """ 1284 | return self._get("canslew") 1285 | 1286 | def canslewaltaz(self): 1287 | """Indicate whether the telescope can slew synchronously to AltAz coordinates. 1288 | 1289 | Returns: 1290 | True if this telescope is capable of programmed slewing (synchronous or 1291 | asynchronous) to local horizontal coordinates. 1292 | 1293 | """ 1294 | return self._get("canslewaltaz") 1295 | 1296 | def canslewaltazasync(self): 1297 | """Indicate whether the telescope can slew asynchronusly to AltAz coordinates. 1298 | 1299 | Returns: 1300 | True if this telescope is capable of programmed asynchronus slewing 1301 | (synchronous or asynchronous) to local horizontal coordinates. 1302 | 1303 | """ 1304 | return self._get("canslewaltazasync") 1305 | 1306 | def cansync(self): 1307 | """Indicate whether the telescope can sync to equatorial coordinates. 1308 | 1309 | Returns: 1310 | True if this telescope is capable of programmed synching to equatorial 1311 | coordinates. 1312 | 1313 | """ 1314 | return self._get("cansync") 1315 | 1316 | def cansyncaltaz(self): 1317 | """Indicate whether the telescope can sync to local horizontal coordinates. 1318 | 1319 | Returns: 1320 | True if this telescope is capable of programmed synching to local horizontal 1321 | coordinates. 1322 | 1323 | """ 1324 | return self._get("cansyncaltaz") 1325 | 1326 | def declination(self): 1327 | """Return the telescope's declination. 1328 | 1329 | Notes: 1330 | Reading the property will raise an error if the value is unavailable. 1331 | 1332 | Returns: 1333 | The declination (degrees) of the telescope's current equatorial coordinates, 1334 | in the coordinate system given by the EquatorialSystem property. 1335 | 1336 | """ 1337 | return self._get("declination") 1338 | 1339 | def declinationrate(self, DeclinationRate: Optional[float] = None): 1340 | """Set or return the telescope's declination tracking rate. 1341 | 1342 | Args: 1343 | DeclinationRate (float): Declination tracking rate (arcseconds per second). 1344 | 1345 | Returns: 1346 | The declination tracking rate (arcseconds per second) if DeclinatioRate is 1347 | not set. 1348 | 1349 | """ 1350 | if DeclinationRate == None: 1351 | return self._get("declinationrate") 1352 | self._put("declinationrate", DeclinationRate=DeclinationRate) 1353 | 1354 | def doesrefraction(self, DoesRefraction: Optional[bool] = None): 1355 | """Indicate or determine if atmospheric refraction is applied to coordinates. 1356 | 1357 | Args: 1358 | DoesRefraction (bool): Set True to make the telescope or driver apply 1359 | atmospheric refraction to coordinates. 1360 | 1361 | Returns: 1362 | True if the telescope or driver applies atmospheric refraction to 1363 | coordinates. 1364 | 1365 | """ 1366 | if DoesRefraction == None: 1367 | return self._get("doesrefraction") 1368 | self._put("doesrefraction", DoesRefraction=DoesRefraction) 1369 | 1370 | def equatorialsystem(self): 1371 | """Return the current equatorial coordinate system used by this telescope. 1372 | 1373 | Returns: 1374 | Current equatorial coordinate system used by this telescope 1375 | (e.g. Topocentric or J2000). 1376 | 1377 | """ 1378 | return self._get("equatorialsystem") 1379 | 1380 | def focallength(self): 1381 | """Return the telescope's focal length in meters. 1382 | 1383 | Returns: 1384 | The telescope's focal length in meters. 1385 | 1386 | """ 1387 | return self._get("focallength") 1388 | 1389 | def guideratedeclination(self, GuideRateDeclination: Optional[float] = None): 1390 | """Set or return the current Declination rate offset for telescope guiding. 1391 | 1392 | Args: 1393 | GuideRateDeclination (float): Declination movement rate offset 1394 | (degrees/sec). 1395 | 1396 | Returns: 1397 | Current declination rate offset for telescope guiding if not set. 1398 | 1399 | """ 1400 | if GuideRateDeclination == None: 1401 | return self._get("guideratedeclination") 1402 | self._put("guideratedeclination", GuideRateDeclination=GuideRateDeclination) 1403 | 1404 | def guideraterightascension(self, GuideRateRightAscension: Optional[float] = None): 1405 | """Set or return the current RightAscension rate offset for telescope guiding. 1406 | 1407 | Args: 1408 | GuideRateRightAscension (float): RightAscension movement rate offset 1409 | (degrees/sec). 1410 | 1411 | Returns: 1412 | Current right ascension rate offset for telescope guiding if not set. 1413 | 1414 | """ 1415 | if GuideRateRightAscension == None: 1416 | return self._get("guideraterightascension") 1417 | self._put( 1418 | "guideraterightascension", GuideRateRightAscension=GuideRateRightAscension 1419 | ) 1420 | 1421 | def ispulseguiding(self): 1422 | """Indicate whether the telescope is currently executing a PulseGuide command. 1423 | 1424 | Returns: 1425 | True if a pulseguide(int, int) command is in progress, False otherwise. 1426 | 1427 | """ 1428 | return self._get("ispulseguiding") 1429 | 1430 | def rightascension(self): 1431 | """Return the telescope's right ascension coordinate. 1432 | 1433 | Returns: 1434 | The right ascension (hours) of the telescope's current equatorial 1435 | coordinates, in the coordinate system given by the EquatorialSystem 1436 | property. 1437 | 1438 | """ 1439 | return self._get("rightascension") 1440 | 1441 | def rightascensionrate(self, RightAscensionRate: Optional[float] = None): 1442 | """Set or return the telescope's right ascension tracking rate. 1443 | 1444 | Args: 1445 | RightAscensionRate (float): Right ascension tracking rate (arcseconds per 1446 | second). 1447 | 1448 | Returns: 1449 | Telescope's right ascension tracking rate if not set. 1450 | 1451 | """ 1452 | if RightAscensionRate == None: 1453 | return self._get("rightascensionrate") 1454 | self._put("rightascensionrate", RightAscensionRate=RightAscensionRate) 1455 | 1456 | def sideofpier(self, SideOfPier: Optional[int] = None): 1457 | """Set or return the mount's pointing state. 1458 | 1459 | Args: 1460 | SideOfPier (int): New pointing state. 0 = pierEast, 1 = pierWest 1461 | 1462 | Returns: 1463 | Side of pier if not set. 1464 | 1465 | """ 1466 | if SideOfPier == None: 1467 | return self._get("sideofpier") 1468 | self._put("sideofpier", SideOfPier=SideOfPier) 1469 | 1470 | def siderealtime(self): 1471 | """Return the local apparent sidereal time. 1472 | 1473 | Returns: 1474 | The local apparent sidereal time from the telescope's internal clock (hours, 1475 | sidereal). 1476 | 1477 | """ 1478 | return self._get("siderealtime") 1479 | 1480 | def siteelevation(self, SiteElevation: Optional[float] = None): 1481 | """Set or return the observing site's elevation above mean sea level. 1482 | 1483 | Args: 1484 | SiteElevation (float): Elevation above mean sea level (metres). 1485 | 1486 | Returns: 1487 | Elevation above mean sea level (metres) of the site at which the telescope 1488 | is located if not set. 1489 | 1490 | """ 1491 | if SiteElevation == None: 1492 | return self._get("siteelevation") 1493 | self._put("siteelevation", SiteElevation=SiteElevation) 1494 | 1495 | def sitelatitude(self, SiteLatitude: Optional[float] = None): 1496 | """Set or return the observing site's latitude. 1497 | 1498 | Args: 1499 | SiteLatitude (float): Site latitude (degrees). 1500 | 1501 | Returns: 1502 | Geodetic(map) latitude (degrees, positive North, WGS84) of the site at which 1503 | the telescope is located if not set. 1504 | 1505 | """ 1506 | if SiteLatitude == None: 1507 | return self._get("sitelatitude") 1508 | self._put("sitelatitude", SiteLatitude=SiteLatitude) 1509 | 1510 | def sitelongitude(self, SiteLongitude: Optional[float] = None): 1511 | """Set or return the observing site's longitude. 1512 | 1513 | Args: 1514 | SiteLongitude (float): Site longitude (degrees, positive East, WGS84) 1515 | 1516 | Returns: 1517 | Longitude (degrees, positive East, WGS84) of the site at which the telescope 1518 | is located. 1519 | 1520 | """ 1521 | if SiteLongitude == None: 1522 | return self._get("sitelongitude") 1523 | self._put("sitelongitude", SiteLongitude=SiteLongitude) 1524 | 1525 | def slewing(self): 1526 | """Indicate whether the telescope is currently slewing. 1527 | 1528 | Returns: 1529 | True if telescope is currently moving in response to one of the Slew methods 1530 | or the moveaxis(int, float) method, False at all other times. 1531 | 1532 | """ 1533 | return self._get("slewing") 1534 | 1535 | def slewsettletime(self, SlewSettleTime: Optional[int] = None): 1536 | """Set or return the post-slew settling time. 1537 | 1538 | Args: 1539 | SlewSettleTime (int): Settling time (integer sec.). 1540 | 1541 | Returns: 1542 | Returns the post-slew settling time (sec.) if not set. 1543 | 1544 | """ 1545 | if SlewSettleTime == None: 1546 | return self._get("slewsettletime") 1547 | self._put("slewsettletime", SlewSettleTime=SlewSettleTime) 1548 | 1549 | def targetdeclination(self, TargetDeclination: Optional[float] = None): 1550 | """Set or return the target declination of a slew or sync. 1551 | 1552 | Args: 1553 | TargetDeclination (float): Target declination(degrees) 1554 | 1555 | Returns: 1556 | Declination (degrees, positive North) for the target of an equatorial slew 1557 | or sync operation. 1558 | 1559 | """ 1560 | if TargetDeclination == None: 1561 | return self._get("targetdeclination") 1562 | self._put("targetdeclination", TargetDeclination=TargetDeclination) 1563 | 1564 | def targetrightascension(self, TargetRightAscension: Optional[float] = None): 1565 | """Set or return the current target right ascension. 1566 | 1567 | Args: 1568 | TargetRightAscension (float): Target right ascension (hours). 1569 | 1570 | Returns: 1571 | Right ascension (hours) for the target of an equatorial slew or sync 1572 | operation. 1573 | 1574 | """ 1575 | if TargetRightAscension == None: 1576 | return self._get("targetrightascension") 1577 | self._put("targetrightascension", TargetRightAscension=TargetRightAscension) 1578 | 1579 | def tracking(self, Tracking: Optional[bool] = None): 1580 | """Enable, disable, or indicate whether the telescope is tracking. 1581 | 1582 | Args: 1583 | Tracking (bool): Tracking enabled / disabled. 1584 | 1585 | Returns: 1586 | State of the telescope's sidereal tracking drive. 1587 | 1588 | """ 1589 | if Tracking == None: 1590 | return self._get("tracking") 1591 | self._put("tracking", Tracking=Tracking) 1592 | 1593 | def trackingrate(self, TrackingRate: Optional[int] = None): 1594 | """Set or return the current tracking rate. 1595 | 1596 | Args: 1597 | TrackingRate (int): New tracking rate. 0 = driveSidereal, 1 = driveLunar, 1598 | 2 = driveSolar, 3 = driveKing. 1599 | 1600 | Returns: 1601 | Current tracking rate of the telescope's sidereal drive if not set. 1602 | 1603 | """ 1604 | if TrackingRate == None: 1605 | return self._get("trackingrate") 1606 | self._put("trackingrate", TrackingRate=TrackingRate) 1607 | 1608 | def trackingrates(self): 1609 | """Return a collection of supported DriveRates values. 1610 | 1611 | Returns: 1612 | List of supported DriveRates values that describe the permissible values of 1613 | the TrackingRate property for this telescope type. 1614 | 1615 | """ 1616 | return self._get("trackingrates") 1617 | 1618 | def utcdate(self, UTCDate: Optional[Union[str, datetime]] = None): 1619 | """Set or return the UTC date/time of the telescope's internal clock. 1620 | 1621 | Args: 1622 | UTCDate: UTC date/time as an str or datetime. 1623 | 1624 | Returns: 1625 | datetime of the UTC date/time if not set. 1626 | 1627 | """ 1628 | if UTCDate == None: 1629 | return dateutil.parser.parse(self._get("utcdate")) 1630 | 1631 | if type(UTCDate) is str: 1632 | data = UTCDate 1633 | elif type(UTCDate) is datetime: 1634 | data = UTCDate.isoformat() 1635 | else: 1636 | raise TypeError() 1637 | 1638 | self._put("utcdate", UTCDate=data) 1639 | 1640 | def abortslew(self): 1641 | """Immediatley stops a slew in progress.""" 1642 | self._put("abortslew") 1643 | 1644 | def axisrates(self, Axis: int): 1645 | """Return rates at which the telescope may be moved about the specified axis. 1646 | 1647 | Returns: 1648 | The rates at which the telescope may be moved about the specified axis by 1649 | the moveaxis(int, float) method. 1650 | 1651 | """ 1652 | return self._get("axisrates", Axis=Axis) 1653 | 1654 | def canmoveaxis(self, Axis: int): 1655 | """Indicate whether the telescope can move the requested axis. 1656 | 1657 | Returns: 1658 | True if this telescope can move the requested axis. 1659 | 1660 | """ 1661 | return self._get("canmoveaxis", Axis=Axis) 1662 | 1663 | def destinationsideofpier(self, RightAscension: float, Declination: float): 1664 | """Predict the pointing state after a German equatorial mount slews to given coordinates. 1665 | 1666 | Args: 1667 | RightAscension (float): Right Ascension coordinate (0.0 to 23.99999999 1668 | hours). 1669 | Declination (float): Declination coordinate (-90.0 to +90.0 degrees). 1670 | 1671 | Returns: 1672 | Pointing state that a German equatorial mount will be in if it slews to the 1673 | given coordinates. The return value will be one of - 0 = pierEast, 1674 | 1 = pierWest, -1 = pierUnknown. 1675 | 1676 | """ 1677 | return self._get( 1678 | "destinationsideofpier", 1679 | RightAscension=RightAscension, 1680 | Declination=Declination, 1681 | ) 1682 | 1683 | def findhome(self): 1684 | """Move the mount to the "home" position.""" 1685 | self._put("findhome") 1686 | 1687 | def moveaxis(self, Axis: int, Rate: float): 1688 | """Move a telescope axis at the given rate. 1689 | 1690 | Args: 1691 | Axis (int): The axis about which rate information is desired. 1692 | 0 = axisPrimary, 1 = axisSecondary, 2 = axisTertiary. 1693 | Rate (float): The rate of motion (deg/sec) about the specified axis 1694 | 1695 | """ 1696 | self._put("moveaxis", Axis=Axis, Rate=Rate) 1697 | 1698 | def park(self): 1699 | """Park the mount.""" 1700 | self._put("park") 1701 | 1702 | def pulseguide(self, Direction: int, Duration: int): 1703 | """Move the scope in the given direction for the given time. 1704 | 1705 | Notes: 1706 | 0 = guideNorth, 1 = guideSouth, 2 = guideEast, 3 = guideWest. 1707 | 1708 | Args: 1709 | Direction (int): Direction in which the guide-rate motion is to be made. 1710 | Duration (int): Duration of the guide-rate motion (milliseconds). 1711 | 1712 | """ 1713 | self._put("pulseguide", Direction=Direction, Duration=Duration) 1714 | 1715 | def setpark(self): 1716 | """Set the telescope's park position.""" 1717 | self._put("setpark") 1718 | 1719 | def slewtoaltaz(self, Azimuth: float, Altitude: float): 1720 | """Slew synchronously to the given local horizontal coordinates. 1721 | 1722 | Args: 1723 | Azimuth (float): Azimuth coordinate (degrees, North-referenced, positive 1724 | East/clockwise). 1725 | Altitude (float): Altitude coordinate (degrees, positive up). 1726 | 1727 | """ 1728 | self._put("slewtoaltaz", Azimuth=Azimuth, Altitude=Altitude) 1729 | 1730 | def slewtoaltazasync(self, Azimuth: float, Altitude: float): 1731 | """Slew asynchronously to the given local horizontal coordinates. 1732 | 1733 | Args: 1734 | Azimuth (float): Azimuth coordinate (degrees, North-referenced, positive 1735 | East/clockwise). 1736 | Altitude (float): Altitude coordinate (degrees, positive up). 1737 | 1738 | """ 1739 | self._put("slewtoaltazasync", Azimuth=Azimuth, Altitude=Altitude) 1740 | 1741 | def slewtocoordinates(self, RightAscension: float, Declination: float): 1742 | """Slew synchronously to the given equatorial coordinates. 1743 | 1744 | Args: 1745 | RightAscension (float): Right Ascension coordinate (hours). 1746 | Declination (float): Declination coordinate (degrees). 1747 | 1748 | """ 1749 | self._put( 1750 | "slewtocoordinates", RightAscension=RightAscension, Declination=Declination 1751 | ) 1752 | 1753 | def slewtocoordinatesasync(self, RightAscension: float, Declination: float): 1754 | """Slew asynchronously to the given equatorial coordinates. 1755 | 1756 | Args: 1757 | RightAscension (float): Right Ascension coordinate (hours). 1758 | Declination (float): Declination coordinate (degrees). 1759 | 1760 | """ 1761 | self._put( 1762 | "slewtocoordinatesasync", 1763 | RightAscension=RightAscension, 1764 | Declination=Declination, 1765 | ) 1766 | 1767 | def slewtotarget(self): 1768 | """Slew synchronously to the TargetRightAscension and TargetDeclination coordinates.""" 1769 | self._put("slewtotarget") 1770 | 1771 | def slewtotargetasync(self): 1772 | """Asynchronously slew to the TargetRightAscension and TargetDeclination coordinates.""" 1773 | self._put("slewtotargetasync") 1774 | 1775 | def synctoaltaz(self, Azimuth: float, Altitude: float): 1776 | """Sync to the given local horizontal coordinates. 1777 | 1778 | Args: 1779 | Azimuth (float): Azimuth coordinate (degrees, North-referenced, positive 1780 | East/clockwise). 1781 | Altitude (float): Altitude coordinate (degrees, positive up). 1782 | 1783 | """ 1784 | self._put("synctoaltaz", Azimuth=Azimuth, Altitude=Altitude) 1785 | 1786 | def synctocoordinates(self, RightAscension: float, Declination: float): 1787 | """Sync to the given equatorial coordinates. 1788 | 1789 | Args: 1790 | RightAscension (float): Right Ascension coordinate (hours). 1791 | Declination (float): Declination coordinate (degrees). 1792 | 1793 | """ 1794 | self._put( 1795 | "synctocoordinates", RightAscension=RightAscension, Declination=Declination 1796 | ) 1797 | 1798 | def synctotarget(self): 1799 | """Sync to the TargetRightAscension and TargetDeclination coordinates.""" 1800 | self._put("synctotarget") 1801 | 1802 | def unpark(self): 1803 | """Unpark the mount.""" 1804 | self._put("unpark") 1805 | 1806 | 1807 | class NumericError(Exception): 1808 | """Exception for when Alpaca throws an error with a numeric value. 1809 | 1810 | Args: 1811 | ErrorNumber (int): Non-zero integer. 1812 | ErrorMessage (str): Message describing the issue that was encountered. 1813 | 1814 | """ 1815 | 1816 | def __init__(self, ErrorNumber: int, ErrorMessage: str): 1817 | """Initialize NumericError object.""" 1818 | super().__init__(self) 1819 | self.message = "Error %d: %s" % (ErrorNumber, ErrorMessage) 1820 | 1821 | def __str__(self): 1822 | """Message to display with error.""" 1823 | return self.message 1824 | 1825 | 1826 | class ErrorMessage(Exception): 1827 | """Exception for when Alpaca throws an error without a numeric value. 1828 | 1829 | Args: 1830 | Value (str): Message describing the issue that was encountered. 1831 | 1832 | """ 1833 | 1834 | def __init__(self, Value: str): 1835 | """Initialize ErrorMessage object.""" 1836 | super().__init__(self) 1837 | self.message = Value 1838 | 1839 | def __str__(self): 1840 | """Message to display with error.""" 1841 | return self.message 1842 | --------------------------------------------------------------------------------