├── .gitignore ├── .readthedocs.yaml ├── .travis.yml ├── CHANGES.rst ├── HACKING ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── bin ├── tellcore_controllers ├── tellcore_events └── tellcore_tool ├── docs ├── Makefile ├── conf.py ├── constants.rst ├── event_example.rst ├── index.rst ├── library.rst ├── make.bat ├── news.rst ├── telldus.rst └── tool_example.rst ├── run_tests ├── setup.py ├── tellcore ├── __init__.py ├── constants.py ├── library.py └── telldus.py └── tests ├── mocklib.py ├── test_library.py └── test_telldus.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | MANIFEST 4 | dist/ 5 | docs/_build/ 6 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "pypy" 9 | - "pypy3" 10 | 11 | install: pip install . 12 | 13 | script: python run_tests 14 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.1.3 (2016-11-22) 5 | ------------------ 6 | 7 | * Added datetime attribute to ``SensorValue``. 8 | 9 | * Fixed strange problem in ``Library`` where the class itself could 10 | sometimes be None in ``__del__``. 11 | 12 | 13 | 1.1.2 (2015-06-24) 14 | ------------------ 15 | 16 | * Added option to ``Library`` to make it possible to select if strings should 17 | be decoded or not. 18 | 19 | * Made tellcore_tool not decode strings (i.e. convert to unicode) when running 20 | under Python 2.x to avoid unicode errors when printing non ascii characters. 21 | 22 | 23 | 1.1.1 (2015-05-01) 24 | ------------------ 25 | 26 | * Fixed a bug that made tellcore_tool not work with Python 3.x. 27 | 28 | 29 | 1.1.0 (2014-11-19) 30 | ------------------ 31 | 32 | * The callback dispatcher is no longer global, but tied to a ``Library`` 33 | instance. Applications wishing to use callbacks must now pass an explicit 34 | dispatcher instance to the ``TelldusCore`` constructor. 35 | 36 | 37 | 1.0.4 (2014-11-05) 38 | ------------------ 39 | 40 | * Made ``last_sent_value`` return an int instead of string. 41 | 42 | 43 | 1.0.3 (2014-02-02) 44 | ------------------ 45 | 46 | * Work around crash in Telldus Core (< v2.1.2) when re-initalizing the library 47 | after ``tdClose``. 48 | 49 | 50 | 1.0.2 (2014-01-28) 51 | ------------------ 52 | 53 | * Packaging fixes. 54 | 55 | 56 | 1.0.1 (2014-01-26) 57 | ------------------ 58 | 59 | * Added ``AsyncioCallbackDispatcher`` class for integrating callbacks with the 60 | new event loop available in Python 3.4 (asyncio). 61 | 62 | * Include tools from bin/ when installing. 63 | 64 | 65 | 1.0.0 (2014-01-09) 66 | ------------------ 67 | 68 | * Added high level support for device groups in the form of the new class 69 | ``DeviceGroup``. 70 | 71 | * More complete documentation. 72 | 73 | * Removed the methods process_callback and process_pending_callbacks from 74 | ``TelldusCore``. Instead, callback_dispatcher is now a public attribute of 75 | ``TelldusCore`` and the default callback dispatcher 76 | ``QueuedCallbackDispatcher`` implements the two methods instead. 77 | 78 | 79 | 0.9.0 (2014-01-03) 80 | ------------------ 81 | 82 | * Telldus functions that used to return bool (``tdSetName``, ``tdSetProtocol``, 83 | ``tdSetModel``, ``tdSetDeviceParameter`` and ``tdRemoveDevice``) now raise an 84 | exception instead of returning False. 85 | 86 | * Support for rain- and windsensors. 87 | 88 | * Include data type in ``SensorValue``. 89 | 90 | 91 | 0.8.0 (2013-08-11) 92 | ------------------ 93 | 94 | * Improved callback handling to simplify integration with different event 95 | loops. Parameter conversion is now done in the library code and the 96 | adaptation to different event loops is done by a simple callback dispatch 97 | class. The default dispatcher (when using ``TelldusCore``) is still done 98 | using a queue. 99 | 100 | * New documentation for parts of the package. Can be read online at 101 | https://tellcore-py.readthedocs.org/. 102 | 103 | * Fix problem with strings and python 3 (issue #2). 104 | 105 | 106 | 0.1.0 (2013-06-26) 107 | ------------------ 108 | 109 | * First release. 110 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | Making a release 2 | ---------------- 3 | 4 | 1. Make sure 'make tests' passes. 5 | 1. Update version in tellcore/__init__.py. 6 | 1. Update CHANGES.rst with changes, version number and date. 7 | 1. Commit all changes and tag (git tag -s -m 'tellcore-py version x.y.z') 8 | 1. Push new tag (git push --tags) 9 | 1. Push new version to Python Package Index: ./setup.py sdist upload 10 | 1. Make new documenation version visible online: 11 | https://readthedocs.org/dashboard/tellcore-py/versions/ 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGES.rst 3 | include LICENSE.txt 4 | include run_tests tests/*.py 5 | include bin/* 6 | prune bin/*~ 7 | include docs/* 8 | prune docs/_build 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | .PHONY: docs 4 | docs: 5 | PYTHONPATH=$(PWD) $(MAKE) -C docs html 6 | firefox docs/_build/html/index.html 7 | 8 | .PHONY: tests 9 | tests: 10 | python2 ./run_tests 11 | python3 ./run_tests 12 | pypy ./run_tests 13 | flake8 tellcore 14 | flake8 bin 15 | # Ignore F403: "from tellcore.constants import *" 16 | # Ignore F405: "X may be undefined, or defined from star imports" 17 | # Ignore E731: "do not assign a lambda expression, use a def" 18 | flake8 --ignore=F403,F405,E731 tests 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python wrapper for Telldus Core 2 | =============================== 3 | 4 | .. image:: https://badge.fury.io/py/tellcore-py.png 5 | :target: https://pypi.python.org/pypi/tellcore-py/ 6 | 7 | .. image:: https://secure.travis-ci.org/erijo/tellcore-py.png?branch=master 8 | :target: http://travis-ci.org/erijo/tellcore-py 9 | 10 | tellcore-py is a Python wrapper for `Telldus' `_ home 11 | automation library `Telldus Core `_. 12 | 13 | * Documentation: https://tellcore-py.readthedocs.org/ 14 | * Official home page: https://github.com/erijo/tellcore-py 15 | * Python package index: https://pypi.python.org/pypi/tellcore-py 16 | 17 | Please report any problem as a `GitHub issue report 18 | `_. 19 | 20 | Features 21 | -------- 22 | 23 | * Wraps the C-interface with a python interface (with classes and exceptions). 24 | * Automatically frees memory for returned strings. 25 | * Throws an exception (TelldusError) in case a library function returns an 26 | error. 27 | * Supports python 3 with automatic handling of strings (i.e. converting between 28 | bytes used by the C-library and strings as used by python). 29 | * Takes care of making callbacks from the library thread-safe. 30 | * Unit tested. 31 | * Besides being useful with the regular Python implementation (a.k.a. `CPython 32 | `_), it also works with `pypy 33 | `_. 34 | * Open source (`GPLv3+ 35 | `_). 36 | * Works on Linux, Mac OS X and Windows. 37 | 38 | Requirements 39 | ------------ 40 | 41 | * Python 2.7, 3.2+ or pypy 42 | * `Telldus Core library `_ 43 | 44 | Installation 45 | ------------ 46 | 47 | .. code-block:: bash 48 | 49 | $ pip install tellcore-py 50 | 51 | Can also be installed by cloning the `GIT repository 52 | `_ or downloading the `ZIP archive 53 | `_ from GitHub and 54 | unpacking it. Then change directory to tellcore-py and run: 55 | 56 | .. code-block:: bash 57 | 58 | $ python setup.py install 59 | 60 | Users 61 | ----- 62 | 63 | * `Home Assistant `_ - Open-source home automation 64 | platform running on Python 3 65 | * `Tellprox `_ - A local server to 66 | use in place of Telldus Live 67 | * `tellive-py `_ - A Python wrapper for 68 | Telldus Live 69 | 70 | Example 71 | ------- 72 | 73 | A simple example for adding a new "lamp" device, turning it on and then turning 74 | all devices off. 75 | 76 | .. code-block:: python 77 | 78 | from tellcore.telldus import TelldusCore 79 | 80 | core = TelldusCore() 81 | lamp = core.add_device("lamp", "arctech", "selflearning-switch", house=12345, unit=1) 82 | lamp.turn_on() 83 | 84 | for device in core.devices(): 85 | device.turn_off() 86 | 87 | More examples can be found in the `bin 88 | `_ directory. 89 | 90 | Internals 91 | --------- 92 | 93 | At the bottom there is the Library class which is a `ctypes 94 | `_ wrapper and closely matches the 95 | API of the underlying Telldus Core library. The library class takes care of 96 | freeing memory returned from the base library and converts errors returned to 97 | TelldusException. The library class is not intended to be used directly. 98 | 99 | Instead, the TelldusCore class provides a more python-ish API on top of the 100 | library class. This class is used for e.g. adding new devices, or enumerating 101 | the existing devices, sensors and/or controllers. E.g. calling the devices() 102 | method returns a list of Device instances. The Device class in turn has methods 103 | for turning the device on, off, etc. 104 | -------------------------------------------------------------------------------- /bin/tellcore_controllers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2013-2014 Erik Johansson 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License as 6 | # published by the Free Software Foundation; either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 17 | # USA 18 | 19 | import sys 20 | 21 | import tellcore.telldus as td 22 | import tellcore.constants as const 23 | 24 | core = td.TelldusCore() 25 | 26 | try: 27 | controllers = core.controllers() 28 | except Exception as e: 29 | print("Error: unable to list controllers (are you using telldus-core >= 2.1.2?)") 30 | print(e) 31 | sys.exit(1) 32 | 33 | TYPES = {const.TELLSTICK_CONTROLLER_TELLSTICK: 'TellStick', 34 | const.TELLSTICK_CONTROLLER_TELLSTICK_DUO: "TellStick Duo", 35 | const.TELLSTICK_CONTROLLER_TELLSTICK_NET: "TellStick Net"} 36 | 37 | for controller in controllers: 38 | try: 39 | name = controller.name 40 | except AttributeError: 41 | name = "" 42 | 43 | print("{}\n name: {}\n serial: {}\n firmware: {}\n available: {}".format( 44 | TYPES[controller.type], name, controller.serial, 45 | controller.firmware, controller.available)) 46 | 47 | print("\nFound {} controller(s)".format(len(controllers))) 48 | -------------------------------------------------------------------------------- /bin/tellcore_events: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2012-2014 Erik Johansson 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License as 6 | # published by the Free Software Foundation; either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 17 | # USA 18 | 19 | import argparse 20 | import sys 21 | 22 | import tellcore.telldus as td 23 | import tellcore.constants as const 24 | 25 | METHODS = {const.TELLSTICK_TURNON: 'turn on', 26 | const.TELLSTICK_TURNOFF: 'turn off', 27 | const.TELLSTICK_BELL: 'bell', 28 | const.TELLSTICK_TOGGLE: 'toggle', 29 | const.TELLSTICK_DIM: 'dim', 30 | const.TELLSTICK_LEARN: 'learn', 31 | const.TELLSTICK_EXECUTE: 'execute', 32 | const.TELLSTICK_UP: 'up', 33 | const.TELLSTICK_DOWN: 'down', 34 | const.TELLSTICK_STOP: 'stop'} 35 | 36 | EVENTS = {const.TELLSTICK_DEVICE_ADDED: "added", 37 | const.TELLSTICK_DEVICE_REMOVED: "removed", 38 | const.TELLSTICK_DEVICE_CHANGED: "changed", 39 | const.TELLSTICK_DEVICE_STATE_CHANGED: "state changed"} 40 | 41 | CHANGES = {const.TELLSTICK_CHANGE_NAME: "name", 42 | const.TELLSTICK_CHANGE_PROTOCOL: "protocol", 43 | const.TELLSTICK_CHANGE_MODEL: "model", 44 | const.TELLSTICK_CHANGE_METHOD: "method", 45 | const.TELLSTICK_CHANGE_AVAILABLE: "available", 46 | const.TELLSTICK_CHANGE_FIRMWARE: "firmware"} 47 | 48 | TYPES = {const.TELLSTICK_CONTROLLER_TELLSTICK: 'tellstick', 49 | const.TELLSTICK_CONTROLLER_TELLSTICK_DUO: "tellstick duo", 50 | const.TELLSTICK_CONTROLLER_TELLSTICK_NET: "tellstick net"} 51 | 52 | 53 | def device_event(id_, method, data, cid): 54 | method_string = METHODS.get(method, "UNKNOWN METHOD {0}".format(method)) 55 | string = "[DEVICE] {0} -> {1}".format(id_, method_string) 56 | if method == const.TELLSTICK_DIM: 57 | string += " [{0}]".format(data) 58 | print(string) 59 | 60 | 61 | def device_change_event(id_, event, type_, cid): 62 | event_string = EVENTS.get(event, "UNKNOWN EVENT {0}".format(event)) 63 | string = "[DEVICE_CHANGE] {0} {1}".format(event_string, id_) 64 | if event == const.TELLSTICK_DEVICE_CHANGED: 65 | type_string = CHANGES.get(type_, "UNKNOWN CHANGE {0}".format(type_)) 66 | string += " [{0}]".format(type_string) 67 | print(string) 68 | 69 | 70 | def raw_event(data, controller_id, cid): 71 | string = "[RAW] {0} <- {1}".format(controller_id, data) 72 | print(string) 73 | 74 | 75 | def sensor_event(protocol, model, id_, dataType, value, timestamp, cid): 76 | string = "[SENSOR] {0} [{1}/{2}] ({3}) @ {4} <- {5}".format( 77 | id_, protocol, model, dataType, timestamp, value) 78 | print(string) 79 | 80 | 81 | def controller_event(id_, event, type_, new_value, cid): 82 | event_string = EVENTS.get(event, "UNKNOWN EVENT {0}".format(event)) 83 | string = "[CONTROLLER] {0} {1}".format(event_string, id_) 84 | if event == const.TELLSTICK_DEVICE_ADDED: 85 | type_string = TYPES.get(type_, "UNKNOWN TYPE {0}".format(type_)) 86 | string += " {0}".format(type_string) 87 | elif (event == const.TELLSTICK_DEVICE_CHANGED 88 | or event == const.TELLSTICK_DEVICE_STATE_CHANGED): 89 | type_string = CHANGES.get(type_, "UNKNOWN CHANGE {0}".format(type_)) 90 | string += " [{0}] -> {1}".format(type_string, new_value) 91 | print(string) 92 | 93 | 94 | parser = argparse.ArgumentParser(description='Listen for Telldus events.') 95 | 96 | parser.add_argument( 97 | '--all', action='store_true', help='Trace all events') 98 | parser.add_argument( 99 | '--device', action='store_true', help='Trace device events') 100 | parser.add_argument( 101 | '--change', action='store_true', help='Trace device change events') 102 | parser.add_argument( 103 | '--raw', action='store_true', help='Trace raw events') 104 | parser.add_argument( 105 | '--sensor', action='store_true', help='Trace sensor events') 106 | parser.add_argument( 107 | '--controller', action='store_true', help='Trace controller events') 108 | 109 | args = vars(parser.parse_args()) 110 | 111 | try: 112 | import asyncio 113 | loop = asyncio.get_event_loop() 114 | dispatcher = td.AsyncioCallbackDispatcher(loop) 115 | except ImportError: 116 | loop = None 117 | dispatcher = td.QueuedCallbackDispatcher() 118 | 119 | core = td.TelldusCore(callback_dispatcher=dispatcher) 120 | callbacks = [] 121 | 122 | for arg in args: 123 | if not (args[arg] or args['all']): 124 | continue 125 | try: 126 | if arg == 'device': 127 | callbacks.append(core.register_device_event(device_event)) 128 | elif arg == 'change': 129 | callbacks.append( 130 | core.register_device_change_event(device_change_event)) 131 | elif arg == 'raw': 132 | callbacks.append(core.register_raw_device_event(raw_event)) 133 | elif arg == 'sensor': 134 | callbacks.append(core.register_sensor_event(sensor_event)) 135 | elif arg == 'controller': 136 | callbacks.append(core.register_controller_event(controller_event)) 137 | else: 138 | assert arg == 'all' 139 | except AttributeError: 140 | if not args['all']: 141 | raise 142 | 143 | if len(callbacks) == 0: 144 | print("Must enable at least one event") 145 | parser.print_usage() 146 | sys.exit(1) 147 | 148 | try: 149 | if loop: 150 | loop.run_forever() 151 | else: 152 | import time 153 | while True: 154 | core.callback_dispatcher.process_pending_callbacks() 155 | time.sleep(0.5) 156 | except KeyboardInterrupt: 157 | pass 158 | -------------------------------------------------------------------------------- /bin/tellcore_tool: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2014 Erik Johansson 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License as 6 | # published by the Free Software Foundation; either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 17 | # USA 18 | 19 | import argparse 20 | import sys 21 | import time 22 | 23 | import tellcore.telldus as td 24 | import tellcore.constants as const 25 | 26 | if sys.version_info < (3, 0): 27 | import tellcore.library as lib 28 | lib.Library.DECODE_STRINGS = False 29 | 30 | def print_devices(devices): 31 | print("Number of devices: {}\n".format(len(devices))) 32 | print("{:<5s} {:<25s} {:<10s} {:<10s} {:<20s} {}".format( 33 | "ID", "NAME", "STATE", "PROTOCOL", "MODEL", "PARAMETERS")) 34 | for device in devices: 35 | cmd = device.last_sent_command( 36 | const.TELLSTICK_TURNON 37 | | const.TELLSTICK_TURNOFF 38 | | const.TELLSTICK_DIM) 39 | if cmd == const.TELLSTICK_TURNON: 40 | cmd_str = "ON" 41 | elif cmd == const.TELLSTICK_TURNOFF: 42 | cmd_str = "OFF" 43 | elif cmd == const.TELLSTICK_DIM: 44 | cmd_str = "DIMMED:{}".format(device.last_sent_value()) 45 | else: 46 | cmd_str = "UNKNOWN:{}".format(cmd) 47 | params_str = "" 48 | for name, value in device.parameters().items(): 49 | params_str += " {}:{}".format(name, value) 50 | print("{:<5d} {:<25s} {:<10s} {:<10s} {:<20s}{}".format( 51 | device.id, device.name, cmd_str, 52 | device.protocol, device.model, params_str)) 53 | 54 | def print_sensors(sensors): 55 | print("Number of sensors: {}\n".format(len(sensors))) 56 | print("{:<15s} {:<15s} {:<5s} {:<8s} {:<8s} {:<18s} {:<20s} {}".format( 57 | "PROTOCOL", "MODEL", "ID", "TEMP", "HUMIDITY", "RAIN", "WIND", 58 | "LAST UPDATED")) 59 | 60 | def format_value(sensor, datatype, formatter): 61 | if not sensor.has_value(datatype): 62 | return ("", None) 63 | value = sensor.value(datatype) 64 | return (formatter(value.value), value.datetime) 65 | 66 | for sensor in sensors: 67 | values = [] 68 | values.append(format_value(sensor, const.TELLSTICK_TEMPERATURE, 69 | lambda x: "{} C".format(x))) 70 | values.append(format_value(sensor, const.TELLSTICK_HUMIDITY, 71 | lambda x: "{} %".format(x))) 72 | values.append(format_value(sensor, const.TELLSTICK_RAINRATE, 73 | lambda x: x + " mm/h ")) 74 | values.append(format_value(sensor, const.TELLSTICK_RAINTOTAL, 75 | lambda x: x + " mm")) 76 | values.append(format_value(sensor, const.TELLSTICK_WINDDIRECTION, 77 | lambda x: ["N", "NNE", "NE", "ENE", 78 | "E", "ESE", "SE", "SSE", 79 | "S", "SSW", "SW", "WSW", 80 | "W", "WNW", "NW", "NNW"] 81 | [int(float(x) / 22.5)] + " ")) 82 | values.append(format_value(sensor, const.TELLSTICK_WINDAVERAGE, 83 | lambda x: x + " m/s ")) 84 | values.append(format_value(sensor, const.TELLSTICK_WINDGUST, 85 | lambda x: "({} m/s)".format(x))) 86 | 87 | # Get first valid timestamp 88 | timestamp = [v[1] for v in values if v[1] is not None][0] 89 | 90 | s = [v[0] for v in values] 91 | values_str = "{:<8s} {:<8s} ".format(s[0], s[1]) 92 | values_str += "{:<18s} ".format(s[2] + s[3]) 93 | values_str += "{:<20s} ".format(s[4] + s[5] + s[6]) 94 | 95 | print("{:<15s} {:<15s} {:<5d} {}{}".format( 96 | sensor.protocol, sensor.model, sensor.id, values_str, 97 | timestamp)) 98 | 99 | def find_device(device, devices): 100 | for d in devices: 101 | if str(d.id) == device or d.name == device: 102 | return d 103 | print("Device '{}' not found".format(device)) 104 | return None 105 | 106 | parser = argparse.ArgumentParser( 107 | description='Telldus administration tool', 108 | epilog='DEVICE can be either device id or name') 109 | group = parser.add_mutually_exclusive_group(required=True) 110 | 111 | group.add_argument( 112 | '-l', '--list', action='store_true', 113 | help='List all configured devices and discovered sensors') 114 | group.add_argument( 115 | '--list-devices', action='store_true', 116 | help='List all configured devices') 117 | group.add_argument( 118 | '--list-sensors', action='store_true', 119 | help='List all discovered sensors') 120 | group.add_argument( 121 | '--on', metavar='DEVICE', help='Turn on device') 122 | group.add_argument( 123 | '--off', metavar='DEVICE', help='Turn off device') 124 | group.add_argument( 125 | '--learn', metavar='DEVICE', help='Send learn command to device') 126 | group.add_argument( 127 | '--remove', metavar='DEVICE', help='Remove device') 128 | 129 | args = vars(parser.parse_args()) 130 | 131 | core = td.TelldusCore() 132 | 133 | if args['list']: 134 | print_devices(core.devices()) 135 | print("") 136 | print_sensors(core.sensors()) 137 | elif args['list_devices']: 138 | print_devices(core.devices()) 139 | elif args['list_sensors']: 140 | print_sensors(core.sensors()) 141 | elif args['on'] is not None: 142 | device = find_device(args['on'], core.devices()) 143 | if device is not None: 144 | device.turn_on() 145 | elif args['off'] is not None: 146 | device = find_device(args['off'], core.devices()) 147 | if device is not None: 148 | device.turn_off() 149 | elif args['learn'] is not None: 150 | device = find_device(args['learn'], core.devices()) 151 | if device is not None: 152 | device.learn() 153 | elif args['remove'] is not None: 154 | device = find_device(args['remove'], core.devices()) 155 | if device is not None: 156 | device.remove() 157 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/tellcore-py.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/tellcore-py.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/tellcore-py" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/tellcore-py" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # tellcore-py documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Aug 6 22:23:53 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | import re 16 | 17 | version_re = re.compile(r'__version__ = "((.*?\..*?)\..*)"') 18 | cwd = os.path.dirname(os.path.abspath(__file__)) 19 | 20 | with open(os.path.join(cwd, '..', 'tellcore', '__init__.py')) as init: 21 | for line in init: 22 | match = version_re.search(line) 23 | if match: 24 | full_version = match.group(1) 25 | short_version = match.group(2) 26 | break 27 | else: 28 | raise Exception('Cannot find version in __init__.py') 29 | 30 | # If extensions (or modules to document with autodoc) are in another directory, 31 | # add these directories to sys.path here. If the directory is relative to the 32 | # documentation root, use os.path.abspath to make it absolute, like shown here. 33 | #sys.path.insert(0, os.path.abspath('.')) 34 | 35 | # -- General configuration ----------------------------------------------------- 36 | 37 | # If your documentation needs a minimal Sphinx version, state it here. 38 | #needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be extensions 41 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 42 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 43 | 44 | # Add any paths that contain templates here, relative to this directory. 45 | templates_path = ['_templates'] 46 | 47 | # The suffix of source filenames. 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = u'tellcore-py' 58 | copyright = u'2014, Erik Johansson' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = short_version 66 | # The full version, including alpha/beta/rc tags. 67 | release = full_version 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | #language = None 72 | 73 | # There are two options for replacing |today|: either, you set today to some 74 | # non-false value, then it is used: 75 | #today = '' 76 | # Else, today_fmt is used as the format for a strftime call. 77 | #today_fmt = '%B %d, %Y' 78 | 79 | # List of patterns, relative to source directory, that match files and 80 | # directories to ignore when looking for source files. 81 | exclude_patterns = ['_build'] 82 | 83 | # The reST default role (used for this markup: `text`) to use for all documents. 84 | #default_role = None 85 | 86 | # If true, '()' will be appended to :func: etc. cross-reference text. 87 | #add_function_parentheses = True 88 | 89 | # If true, the current module name will be prepended to all description 90 | # unit titles (such as .. function::). 91 | #add_module_names = True 92 | 93 | # If true, sectionauthor and moduleauthor directives will be shown in the 94 | # output. They are ignored by default. 95 | #show_authors = False 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = 'sphinx' 99 | 100 | # A list of ignored prefixes for module index sorting. 101 | #modindex_common_prefix = [] 102 | 103 | 104 | # -- Options for HTML output --------------------------------------------------- 105 | 106 | # The theme to use for HTML and HTML Help pages. See the documentation for 107 | # a list of builtin themes. 108 | html_theme = 'default' 109 | 110 | # Theme options are theme-specific and customize the look and feel of a theme 111 | # further. For a list of options available for each theme, see the 112 | # documentation. 113 | #html_theme_options = {} 114 | 115 | # Add any paths that contain custom themes here, relative to this directory. 116 | #html_theme_path = [] 117 | 118 | # The name for this set of Sphinx documents. If None, it defaults to 119 | # " v documentation". 120 | #html_title = None 121 | 122 | # A shorter title for the navigation bar. Default is the same as html_title. 123 | #html_short_title = None 124 | 125 | # The name of an image file (relative to this directory) to place at the top 126 | # of the sidebar. 127 | #html_logo = None 128 | 129 | # The name of an image file (within the static path) to use as favicon of the 130 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 131 | # pixels large. 132 | #html_favicon = None 133 | 134 | # Add any paths that contain custom static files (such as style sheets) here, 135 | # relative to this directory. They are copied after the builtin static files, 136 | # so a file named "default.css" will overwrite the builtin "default.css". 137 | #html_static_path = ['_static'] 138 | 139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 140 | # using the given strftime format. 141 | #html_last_updated_fmt = '%b %d, %Y' 142 | 143 | # If true, SmartyPants will be used to convert quotes and dashes to 144 | # typographically correct entities. 145 | #html_use_smartypants = True 146 | 147 | # Custom sidebar templates, maps document names to template names. 148 | #html_sidebars = {} 149 | 150 | # Additional templates that should be rendered to pages, maps page names to 151 | # template names. 152 | #html_additional_pages = {} 153 | 154 | # If false, no module index is generated. 155 | #html_domain_indices = True 156 | 157 | # If false, no index is generated. 158 | #html_use_index = True 159 | 160 | # If true, the index is split into individual pages for each letter. 161 | #html_split_index = False 162 | 163 | # If true, links to the reST sources are added to the pages. 164 | #html_show_sourcelink = True 165 | 166 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 167 | #html_show_sphinx = True 168 | 169 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 170 | #html_show_copyright = True 171 | 172 | # If true, an OpenSearch description file will be output, and all pages will 173 | # contain a tag referring to it. The value of this option must be the 174 | # base URL from which the finished HTML is served. 175 | #html_use_opensearch = '' 176 | 177 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 178 | #html_file_suffix = None 179 | 180 | # Output file base name for HTML help builder. 181 | htmlhelp_basename = 'tellcore-pydoc' 182 | 183 | 184 | # -- Options for LaTeX output -------------------------------------------------- 185 | 186 | latex_elements = { 187 | # The paper size ('letterpaper' or 'a4paper'). 188 | #'papersize': 'letterpaper', 189 | 190 | # The font size ('10pt', '11pt' or '12pt'). 191 | #'pointsize': '10pt', 192 | 193 | # Additional stuff for the LaTeX preamble. 194 | #'preamble': '', 195 | } 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, author, documentclass [howto/manual]). 199 | latex_documents = [ 200 | ('index', 'tellcore-py.tex', u'tellcore-py Documentation', 201 | u'Erik Johansson', 'manual'), 202 | ] 203 | 204 | # The name of an image file (relative to this directory) to place at the top of 205 | # the title page. 206 | #latex_logo = None 207 | 208 | # For "manual" documents, if this is true, then toplevel headings are parts, 209 | # not chapters. 210 | #latex_use_parts = False 211 | 212 | # If true, show page references after internal links. 213 | #latex_show_pagerefs = False 214 | 215 | # If true, show URL addresses after external links. 216 | #latex_show_urls = False 217 | 218 | # Documents to append as an appendix to all manuals. 219 | #latex_appendices = [] 220 | 221 | # If false, no module index is generated. 222 | #latex_domain_indices = True 223 | 224 | 225 | # -- Options for manual page output -------------------------------------------- 226 | 227 | # One entry per manual page. List of tuples 228 | # (source start file, name, description, authors, manual section). 229 | man_pages = [ 230 | ('index', 'tellcore-py', u'tellcore-py Documentation', 231 | [u'Erik Johansson'], 1) 232 | ] 233 | 234 | # If true, show URL addresses after external links. 235 | #man_show_urls = False 236 | 237 | 238 | # -- Options for Texinfo output ------------------------------------------------ 239 | 240 | # Grouping the document tree into Texinfo files. List of tuples 241 | # (source start file, target name, title, author, 242 | # dir menu entry, description, category) 243 | texinfo_documents = [ 244 | ('index', 'tellcore-py', u'tellcore-py Documentation', 245 | u'Erik Johansson', 'tellcore-py', 'One line description of project.', 246 | 'Miscellaneous'), 247 | ] 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #texinfo_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #texinfo_domain_indices = True 254 | 255 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 256 | #texinfo_show_urls = 'footnote' 257 | -------------------------------------------------------------------------------- /docs/constants.rst: -------------------------------------------------------------------------------- 1 | tellcore.constants (module) 2 | =========================== 3 | 4 | .. automodule:: tellcore.constants 5 | 6 | .. literalinclude:: ../tellcore/constants.py 7 | :start-after: # Tellstick constants 8 | -------------------------------------------------------------------------------- /docs/event_example.rst: -------------------------------------------------------------------------------- 1 | .. _event-example: 2 | 3 | Event handling (example) 4 | ======================== 5 | 6 | The example below illustrates how event callbacks are registered and processed 7 | using the default :class:`.telldus.QueuedCallbackDispatcher` dispatcher. 8 | 9 | For more information regarding the arguments to the callback functions, please 10 | refere to the official `Telldus Core documentation 11 | `_ (see the callback 12 | typedefs). Please note that the context mentioned there is not included in 13 | tellcore-py. 14 | 15 | .. literalinclude:: ../bin/tellcore_events 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to tellcore-py's documentation! 2 | ======================================= 3 | 4 | tellcore-py is a Python wrapper for `Telldus' `_ home 5 | automation library `Telldus Core `_. 6 | 7 | Contents: 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | telldus 13 | constants 14 | library 15 | tool_example 16 | event_example 17 | news 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /docs/library.rst: -------------------------------------------------------------------------------- 1 | tellcore.library (module) 2 | ========================= 3 | 4 | The classes in this module are not meant to be used directly. They are mostly 5 | support classes for the higher level API described in :doc:`telldus`. 6 | 7 | .. automodule:: tellcore.library 8 | 9 | Library 10 | ------- 11 | .. autoclass:: Library(name=None, callback_dispatcher=None) 12 | :members: 13 | 14 | .. automethod:: __init__(name=None, callback_dispatcher=None) 15 | 16 | .. automethod:: __del__ 17 | 18 | TelldusError 19 | ------------ 20 | .. autoexception:: TelldusError 21 | :members: 22 | 23 | .. automethod:: __str__ 24 | 25 | BaseCallbackDispatcher 26 | ---------------------- 27 | .. autoclass:: BaseCallbackDispatcher 28 | :members: 29 | 30 | DirectCallbackDispatcher 31 | ------------------------ 32 | .. autoclass:: DirectCallbackDispatcher 33 | :members: 34 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\tellcore-py.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\tellcore-py.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/news.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/telldus.rst: -------------------------------------------------------------------------------- 1 | tellcore.telldus (module) 2 | ========================= 3 | 4 | .. automodule:: tellcore.telldus 5 | 6 | This module provides a high level Python interface to Telldus' C API. 7 | 8 | Since most functions are Python-ified wrappers around the C API, please also 9 | refer to the `Telldus Core documentation 10 | `_ for further 11 | information. 12 | 13 | The classes in this module all use the :class:`.library.Library` class under 14 | the hood, and thus also has e.g. automatic memory manangement, exceptions 15 | (:exc:`.library.TelldusError`) and transparent string conversion with full 16 | Python 3 support. 17 | 18 | Some example programs are included in the documentation to help understand how 19 | to use the different classes: 20 | 21 | * :ref:`tool-example` 22 | * :ref:`event-example` 23 | 24 | TelldusCore 25 | ----------- 26 | .. autoclass:: TelldusCore 27 | :members: 28 | 29 | .. automethod:: __init__ 30 | 31 | .. attribute:: callback_dispatcher 32 | 33 | The callback dispatcher used. Set when constructing the instance and 34 | should not be changed. 35 | 36 | DeviceFactory 37 | ------------- 38 | .. autofunction:: DeviceFactory 39 | 40 | Device 41 | ------ 42 | .. autoclass:: Device 43 | :members: 44 | 45 | .. attribute:: name 46 | 47 | The name of the device (read/write). 48 | 49 | .. attribute:: protocol 50 | 51 | The protocol used for the device (read/write). 52 | 53 | .. attribute:: model 54 | 55 | The device's model (read/write). 56 | 57 | .. attribute:: type 58 | 59 | The device type (read only). One of the device type constants from 60 | :mod:`tellcore.constants`. 61 | 62 | DeviceGroup 63 | ----------- 64 | .. autoclass:: DeviceGroup 65 | :members: 66 | 67 | Sensor 68 | ------ 69 | .. autoclass:: Sensor 70 | :members: 71 | 72 | SensorValue 73 | ----------- 74 | .. autoclass:: SensorValue 75 | :members: 76 | 77 | .. attribute:: datatype 78 | 79 | One of the sensor value type constants from :mod:`tellcore.constants`. 80 | 81 | .. attribute:: value 82 | 83 | The sensor value. 84 | 85 | .. attribute:: timestamp 86 | 87 | The time the sensor value was registered (in seconds since epoch). 88 | 89 | Controller 90 | ---------- 91 | .. autoclass:: Controller 92 | :members: 93 | 94 | .. attribute:: id 95 | 96 | .. attribute:: type 97 | 98 | One of the controller type constants from :mod:`tellcore.constants`. 99 | 100 | QueuedCallbackDispatcher 101 | ------------------------ 102 | .. autoclass:: QueuedCallbackDispatcher 103 | :members: 104 | 105 | AsyncioCallbackDispatcher 106 | ------------------------- 107 | .. autoclass:: AsyncioCallbackDispatcher 108 | :members: 109 | -------------------------------------------------------------------------------- /docs/tool_example.rst: -------------------------------------------------------------------------------- 1 | .. _tool-example: 2 | 3 | tellcore_tool (example) 4 | ======================= 5 | 6 | The example below is an implementation of tdtool similar to the one included in 7 | the official Telldus Core distribution. 8 | 9 | .. literalinclude:: ../bin/tellcore_tool 10 | -------------------------------------------------------------------------------- /run_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import unittest 5 | 6 | tests = unittest.TestLoader().discover("tests") 7 | res = unittest.runner.TextTestRunner().run(tests) 8 | 9 | sys.exit(not res.wasSuccessful()) 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | import os 5 | import re 6 | 7 | version_re = re.compile(r'__version__ = "(.*)"') 8 | cwd = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | with open(os.path.join(cwd, 'tellcore', '__init__.py')) as init: 11 | for line in init: 12 | match = version_re.search(line) 13 | if match: 14 | version = match.group(1) 15 | break 16 | else: 17 | raise Exception('Cannot find version in __init__.py') 18 | 19 | setup( 20 | name='tellcore-py', 21 | version=version, 22 | author='Erik Johansson', 23 | author_email='erik@ejohansson.se', 24 | packages=['tellcore'], 25 | provides=['tellcore'], 26 | scripts=['bin/tellcore_tool', 'bin/tellcore_events', 27 | 'bin/tellcore_controllers'], 28 | url='https://github.com/erijo/tellcore-py', 29 | license='GPLv3+', 30 | description='Python wrapper for Telldus\' home automation library', 31 | long_description=open('README.rst').read() + '\n\n' + \ 32 | open('CHANGES.rst').read(), 33 | classifiers=[ 34 | 'Development Status :: 5 - Production/Stable', 35 | 'Intended Audience :: Developers', 36 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 37 | 'Operating System :: OS Independent', 38 | 'Programming Language :: Python :: 2', 39 | 'Programming Language :: Python :: 2.7', 40 | 'Programming Language :: Python :: 3', 41 | 'Programming Language :: Python :: 3.2', 42 | 'Programming Language :: Python :: 3.3', 43 | 'Programming Language :: Python :: 3.4', 44 | 'Topic :: Home Automation', 45 | 'Topic :: Software Development :: Libraries :: Python Modules', 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /tellcore/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1.3" 2 | -------------------------------------------------------------------------------- /tellcore/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012-2014 Erik Johansson 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License as 5 | # published by the Free Software Foundation; either version 3 of the 6 | # License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 16 | # USA 17 | 18 | """ 19 | Contains all constants used in the API. 20 | 21 | All the TELLSTICK_* defines documented in the `Telldus Core documentation 22 | `_ are available here. 23 | 24 | The recommended usage is to do:: 25 | import tellcore.constants as const 26 | 27 | The constants will then be available as const.TELLSTICK_TURNON, etc. See below 28 | for the full list of all available constants. 29 | """ 30 | 31 | # Tellstick constants 32 | # Device methods 33 | TELLSTICK_TURNON = 1 34 | TELLSTICK_TURNOFF = 2 35 | TELLSTICK_BELL = 4 36 | TELLSTICK_TOGGLE = 8 37 | TELLSTICK_DIM = 16 38 | TELLSTICK_LEARN = 32 39 | TELLSTICK_EXECUTE = 64 40 | TELLSTICK_UP = 128 41 | TELLSTICK_DOWN = 256 42 | TELLSTICK_STOP = 512 43 | 44 | # Sensor value types 45 | TELLSTICK_TEMPERATURE = 1 46 | TELLSTICK_HUMIDITY = 2 47 | TELLSTICK_RAINRATE = 4 48 | TELLSTICK_RAINTOTAL = 8 49 | TELLSTICK_WINDDIRECTION = 16 50 | TELLSTICK_WINDAVERAGE = 32 51 | TELLSTICK_WINDGUST = 64 52 | 53 | # Error codes 54 | TELLSTICK_SUCCESS = 0 55 | TELLSTICK_ERROR_NOT_FOUND = -1 56 | TELLSTICK_ERROR_PERMISSION_DENIED = -2 57 | TELLSTICK_ERROR_DEVICE_NOT_FOUND = -3 58 | TELLSTICK_ERROR_METHOD_NOT_SUPPORTED = -4 59 | TELLSTICK_ERROR_COMMUNICATION = -5 60 | TELLSTICK_ERROR_CONNECTING_SERVICE = -6 61 | TELLSTICK_ERROR_UNKNOWN_RESPONSE = -7 62 | TELLSTICK_ERROR_SYNTAX = -8 63 | TELLSTICK_ERROR_BROKEN_PIPE = -9 64 | TELLSTICK_ERROR_COMMUNICATING_SERVICE = -10 65 | TELLSTICK_ERROR_CONFIG_SYNTAX = -11 66 | TELLSTICK_ERROR_UNKNOWN = -99 67 | 68 | # Device types 69 | TELLSTICK_TYPE_DEVICE = 1 70 | TELLSTICK_TYPE_GROUP = 2 71 | TELLSTICK_TYPE_SCENE = 3 72 | 73 | # Controller types 74 | TELLSTICK_CONTROLLER_TELLSTICK = 1 75 | TELLSTICK_CONTROLLER_TELLSTICK_DUO = 2 76 | TELLSTICK_CONTROLLER_TELLSTICK_NET = 3 77 | 78 | # Device changes 79 | TELLSTICK_DEVICE_ADDED = 1 80 | TELLSTICK_DEVICE_CHANGED = 2 81 | TELLSTICK_DEVICE_REMOVED = 3 82 | TELLSTICK_DEVICE_STATE_CHANGED = 4 83 | 84 | # Change types 85 | TELLSTICK_CHANGE_NAME = 1 86 | TELLSTICK_CHANGE_PROTOCOL = 2 87 | TELLSTICK_CHANGE_MODEL = 3 88 | TELLSTICK_CHANGE_METHOD = 4 89 | TELLSTICK_CHANGE_AVAILABLE = 5 90 | TELLSTICK_CHANGE_FIRMWARE = 6 91 | -------------------------------------------------------------------------------- /tellcore/library.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012-2014 Erik Johansson 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License as 5 | # published by the Free Software Foundation; either version 3 of the 6 | # License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 16 | # USA 17 | 18 | from ctypes import c_bool, c_char_p, c_int, c_ubyte, c_void_p 19 | from ctypes import byref, cast, create_string_buffer, POINTER, sizeof 20 | import platform 21 | import threading 22 | 23 | import tellcore.constants as const 24 | 25 | 26 | if platform.system() == 'Windows': 27 | from ctypes import WINFUNCTYPE as FUNCTYPE, windll as DllLoader 28 | LIBRARY_NAME = 'TelldusCore.dll' 29 | else: 30 | from ctypes import CFUNCTYPE as FUNCTYPE, cdll as DllLoader 31 | if platform.system() == 'Darwin': 32 | from ctypes.util import find_library 33 | LIBRARY_NAME = find_library('TelldusCore') or \ 34 | '/Library/Frameworks/TelldusCore.framework/TelldusCore' 35 | else: 36 | LIBRARY_NAME = 'libtelldus-core.so.2' 37 | 38 | DEVICE_EVENT_FUNC = FUNCTYPE( 39 | None, c_int, c_int, c_char_p, c_int, c_void_p) 40 | DEVICE_CHANGE_EVENT_FUNC = FUNCTYPE( 41 | None, c_int, c_int, c_int, c_int, c_void_p) 42 | RAW_DEVICE_EVENT_FUNC = FUNCTYPE( 43 | None, c_char_p, c_int, c_int, c_void_p) 44 | SENSOR_EVENT_FUNC = FUNCTYPE( 45 | None, c_char_p, c_char_p, c_int, c_int, c_char_p, c_int, c_int, c_void_p) 46 | CONTROLLER_EVENT_FUNC = FUNCTYPE( 47 | None, c_int, c_int, c_int, c_char_p, c_int, c_void_p) 48 | 49 | 50 | class TelldusError(Exception): 51 | """Error returned from Telldus Core API. 52 | 53 | Automatically raised when a function in the C API returns an error. 54 | 55 | Attributes: 56 | error: The error code constant (one of TELLSTICK_ERROR_* from 57 | :mod:`tellcore.constants`). 58 | """ 59 | 60 | def __init__(self, error, lib=None): 61 | super(TelldusError, self).__init__() 62 | self.error = error 63 | self.lib = lib or Library() 64 | 65 | def __str__(self): 66 | """Return the human readable error string.""" 67 | msg = self.lib.tdGetErrorString(self.error) 68 | return "%s (%d)" % (msg, self.error) 69 | 70 | 71 | class BaseCallbackDispatcher(object): 72 | """Base callback dispatcher class. 73 | 74 | Inherit from this class and override the :func:`on_callback` method to 75 | change how callbacks are dispatched. 76 | """ 77 | 78 | def on_callback(self, callback, *args): 79 | """Called from the callback thread when an event is received. 80 | 81 | :param callable callback: The callback function to call. 82 | :param args: The arguments to pass to the callback. 83 | """ 84 | raise NotImplementedError 85 | 86 | 87 | class DirectCallbackDispatcher(BaseCallbackDispatcher): 88 | """Dispatches callbacks directly. 89 | 90 | Since the callback is dispatched directly, the callback is called in the 91 | callback thread. 92 | """ 93 | 94 | def on_callback(self, callback, *args): 95 | callback(*args) 96 | 97 | 98 | class Library(object): 99 | """Wrapper around the Telldus Core C API. 100 | 101 | With the exception of tdInit, tdClose and tdReleaseString, all functions in 102 | the C API (see `Telldus Core documentation 103 | `_) can be 104 | called. The parameters are the same as in the C API documentation. The 105 | return value are mostly the same as for the C API, except for functions 106 | with multiple out parameters. 107 | 108 | In addition, this class: 109 | * automatically frees memory for strings returned from the C API, 110 | * converts errors returned from functions into 111 | (:class:`TelldusError`) exceptions, 112 | * transparently converts between Python strings and C style strings. 113 | """ 114 | 115 | STRING_ENCODING = 'utf-8' 116 | DECODE_STRINGS = True 117 | 118 | class c_string_p(c_char_p): 119 | def __init__(self, param): 120 | c_char_p.__init__(self, param.encode(Library.STRING_ENCODING)) 121 | 122 | @classmethod 123 | def from_param(cls, param): 124 | if type(param) is str: 125 | return cls(param) 126 | try: 127 | if type(param) is unicode: 128 | return cls(param) 129 | except NameError: 130 | pass # The unicode type does not exist in python 3 131 | return c_char_p.from_param(param) 132 | 133 | # Must be a separate class (i.e. not part of Library), to avoid circular 134 | # references when saving the wrapper callback function in a class with a 135 | # destructor, as the destructor is not called in that case. 136 | class CallbackWrapper(object): 137 | def __init__(self, dispatcher): 138 | self._callbacks = {} 139 | self._lock = threading.Lock() 140 | self._dispatcher = dispatcher 141 | 142 | def get_callback_ids(self): 143 | with self._lock: 144 | return list(self._callbacks.keys()) 145 | 146 | def register_callback(self, registrator, functype, callback): 147 | wrapper = functype(self._callback) 148 | with self._lock: 149 | cid = registrator(wrapper, None) 150 | self._callbacks[cid] = (wrapper, callback) 151 | return cid 152 | 153 | def unregister_callback(self, cid): 154 | with self._lock: 155 | del self._callbacks[cid] 156 | 157 | def _callback(self, *in_args): 158 | args = [] 159 | # Convert all char* parameters (i.e. bytes) to proper python 160 | # strings 161 | for arg in in_args: 162 | if type(arg) is bytes: 163 | args.append(arg.decode(Library.STRING_ENCODING)) 164 | else: 165 | args.append(arg) 166 | 167 | # Get the real callback and the dispatcher 168 | with self._lock: 169 | try: 170 | # args[-2] is callback id 171 | (wrapper, callback) = self._callbacks[args[-2]] 172 | except KeyError: 173 | return 174 | dispatcher = self._dispatcher 175 | 176 | # Dispatch the callback, dropping the last parameter which is the 177 | # context and always None. 178 | try: 179 | dispatcher.on_callback(callback, *args[:-1]) 180 | except: 181 | pass 182 | 183 | _lib = None 184 | _refcount = 0 185 | 186 | _functions = { 187 | 'tdInit': [None, []], 188 | 'tdClose': [None, []], 189 | 'tdReleaseString': [None, [c_void_p]], 190 | 'tdGetErrorString': [c_char_p, [c_int]], 191 | 192 | 'tdRegisterDeviceEvent': 193 | [c_int, [DEVICE_EVENT_FUNC, c_void_p]], 194 | 'tdRegisterDeviceChangeEvent': 195 | [c_int, [DEVICE_CHANGE_EVENT_FUNC, c_void_p]], 196 | 'tdRegisterRawDeviceEvent': 197 | [c_int, [RAW_DEVICE_EVENT_FUNC, c_void_p]], 198 | 'tdRegisterSensorEvent': 199 | [c_int, [SENSOR_EVENT_FUNC, c_void_p]], 200 | 'tdRegisterControllerEvent': 201 | [c_int, [CONTROLLER_EVENT_FUNC, c_void_p]], 202 | 'tdUnregisterCallback': [c_int, [c_int]], 203 | 204 | 'tdTurnOn': [c_int, [c_int]], 205 | 'tdTurnOff': [c_int, [c_int]], 206 | 'tdBell': [c_int, [c_int]], 207 | 'tdDim': [c_int, [c_int, c_ubyte]], 208 | 'tdExecute': [c_int, [c_int]], 209 | 'tdUp': [c_int, [c_int]], 210 | 'tdDown': [c_int, [c_int]], 211 | 'tdStop': [c_int, [c_int]], 212 | 'tdLearn': [c_int, [c_int]], 213 | 'tdMethods': [c_int, [c_int, c_int]], 214 | 'tdLastSentCommand': [c_int, [c_int, c_int]], 215 | 'tdLastSentValue': [c_char_p, [c_int]], 216 | 217 | 'tdGetNumberOfDevices': [c_int, []], 218 | 'tdGetDeviceId': [c_int, [c_int]], 219 | 'tdGetDeviceType': [c_int, [c_int]], 220 | 221 | 'tdGetName': [c_char_p, [c_int]], 222 | 'tdSetName': [c_bool, [c_int, c_string_p]], 223 | 'tdGetProtocol': [c_char_p, [c_int]], 224 | 'tdSetProtocol': [c_bool, [c_int, c_string_p]], 225 | 'tdGetModel': [c_char_p, [c_int]], 226 | 'tdSetModel': [c_bool, [c_int, c_string_p]], 227 | 228 | 'tdGetDeviceParameter': [c_char_p, [c_int, c_string_p, c_string_p]], 229 | 'tdSetDeviceParameter': [c_bool, [c_int, c_string_p, c_string_p]], 230 | 231 | 'tdAddDevice': [c_int, []], 232 | 'tdRemoveDevice': [c_bool, [c_int]], 233 | 234 | 'tdSendRawCommand': [c_int, [c_string_p, c_int]], 235 | 236 | 'tdConnectTellStickController': [None, [c_int, c_int, c_string_p]], 237 | 'tdDisconnectTellStickController': [None, [c_int, c_int, c_string_p]], 238 | 239 | 'tdSensor': [c_int, [c_char_p, c_int, c_char_p, c_int, 240 | POINTER(c_int), POINTER(c_int)]], 241 | 'tdSensorValue': [c_int, [c_string_p, c_string_p, c_int, c_int, 242 | c_char_p, c_int, POINTER(c_int)]], 243 | 244 | 'tdController': [c_int, [POINTER(c_int), POINTER(c_int), 245 | c_char_p, c_int, POINTER(c_int)]], 246 | 'tdControllerValue': [c_int, [c_int, c_string_p, c_char_p, c_int]], 247 | 'tdSetControllerValue': [c_int, [c_int, c_string_p, c_string_p]], 248 | 'tdRemoveController': [c_int, [c_int]], 249 | } 250 | 251 | def _to_str(self, char_p): 252 | return char_p.value.decode(Library.STRING_ENCODING) 253 | 254 | def _setup_functions(self, lib): 255 | def check_int_result(result, func, args): 256 | if result < 0: 257 | raise TelldusError(result) 258 | return result 259 | 260 | def check_bool_result(result, func, args): 261 | if not result: 262 | raise TelldusError(const.TELLSTICK_ERROR_DEVICE_NOT_FOUND) 263 | return result 264 | 265 | def free_string(result, func, args): 266 | string = cast(result, c_char_p).value 267 | if string is not None: 268 | lib.tdReleaseString(result) 269 | if Library.DECODE_STRINGS: 270 | string = string.decode(Library.STRING_ENCODING) 271 | return string 272 | 273 | for name, signature in Library._functions.items(): 274 | try: 275 | func = getattr(lib, name) 276 | func.restype = signature[0] 277 | func.argtypes = signature[1] 278 | 279 | if func.restype == c_int: 280 | func.errcheck = check_int_result 281 | elif func.restype == c_bool: 282 | func.errcheck = check_bool_result 283 | elif func.restype == c_char_p: 284 | func.restype = c_void_p 285 | func.errcheck = free_string 286 | except AttributeError: 287 | # Older version of the lib don't have all the functions 288 | pass 289 | 290 | def __init__(self, name=None, callback_dispatcher=None): 291 | """Load and initialize the Telldus core library. 292 | 293 | The underlaying library is only initialized the first time this object 294 | is created. Subsequent instances uses the same underlaying library 295 | instance. 296 | 297 | :param str name: If None than the platform specific name of the 298 | Telldus library is used, but it can be e.g. an absolute path. 299 | 300 | :param callback_dispatcher: If callbacks are to be used, this parameter 301 | must refer to an instance of a class inheriting from 302 | :class:`BaseCallbackDispatcher`. 303 | 304 | """ 305 | super(Library, self).__init__() 306 | 307 | if not Library._lib: 308 | assert Library._refcount == 0 309 | if name is None: 310 | name = LIBRARY_NAME 311 | 312 | lib = DllLoader.LoadLibrary(name) 313 | self._setup_functions(lib) 314 | lib.tdInit() 315 | Library._lib = lib 316 | 317 | Library._refcount += 1 318 | if callback_dispatcher is not None: 319 | self._callback_wrapper = Library.CallbackWrapper( 320 | callback_dispatcher) 321 | else: 322 | self._callback_wrapper = None 323 | 324 | def __del__(self): 325 | """Close and unload the Telldus core library. 326 | 327 | Any callback set up is removed. 328 | 329 | The underlaying library is only closed and unloaded if this is the last 330 | instance sharing the same underlaying library instance. 331 | """ 332 | # Using self.__class__.* instead of Library.* here to avoid a 333 | # strange problem where Library could, in some runs, be None. 334 | 335 | # Happens if the LoadLibrary call fails 336 | if self.__class__._lib is None: 337 | assert self.__class__._refcount == 0 338 | return 339 | 340 | assert self.__class__._refcount >= 1 341 | self.__class__._refcount -= 1 342 | 343 | if self._callback_wrapper is not None: 344 | for cid in self._callback_wrapper.get_callback_ids(): 345 | try: 346 | self.tdUnregisterCallback(cid) 347 | except: 348 | pass 349 | 350 | if self.__class__._refcount != 0: 351 | return 352 | 353 | # telldus-core before v2.1.2 (where tdController was added) does not 354 | # handle re-initialization after tdClose has been called (see Telldus 355 | # ticket 188). 356 | if hasattr(self.__class__._lib, "tdController"): 357 | self.__class__._lib.tdClose() 358 | self.__class__._lib = None 359 | 360 | def __getattr__(self, name): 361 | if name == 'callback_dispatcher': 362 | return self._callback_wrapper._dispatcher 363 | if name in Library._functions: 364 | return getattr(self._lib, name) 365 | raise AttributeError(name) 366 | 367 | def tdInit(self): 368 | raise NotImplementedError('should not be called explicitly') 369 | 370 | def tdClose(self): 371 | raise NotImplementedError('should not be called explicitly') 372 | 373 | def tdReleaseString(self, string): 374 | raise NotImplementedError('should not be called explicitly') 375 | 376 | def tdRegisterDeviceEvent(self, callback): 377 | assert(self._callback_wrapper is not None) 378 | return self._callback_wrapper.register_callback( 379 | self._lib.tdRegisterDeviceEvent, DEVICE_EVENT_FUNC, callback) 380 | 381 | def tdRegisterDeviceChangeEvent(self, callback): 382 | assert(self._callback_wrapper is not None) 383 | return self._callback_wrapper.register_callback( 384 | self._lib.tdRegisterDeviceChangeEvent, DEVICE_CHANGE_EVENT_FUNC, 385 | callback) 386 | 387 | def tdRegisterRawDeviceEvent(self, callback): 388 | assert(self._callback_wrapper is not None) 389 | return self._callback_wrapper.register_callback( 390 | self._lib.tdRegisterRawDeviceEvent, RAW_DEVICE_EVENT_FUNC, 391 | callback) 392 | 393 | def tdRegisterSensorEvent(self, callback): 394 | assert(self._callback_wrapper is not None) 395 | return self._callback_wrapper.register_callback( 396 | self._lib.tdRegisterSensorEvent, SENSOR_EVENT_FUNC, callback) 397 | 398 | def tdRegisterControllerEvent(self, callback): 399 | assert(self._callback_wrapper is not None) 400 | return self._callback_wrapper.register_callback( 401 | self._lib.tdRegisterControllerEvent, CONTROLLER_EVENT_FUNC, 402 | callback) 403 | 404 | def tdUnregisterCallback(self, cid): 405 | assert(self._callback_wrapper is not None) 406 | self._callback_wrapper.unregister_callback(cid) 407 | self._lib.tdUnregisterCallback(cid) 408 | 409 | def tdSensor(self): 410 | """Get the next sensor while iterating. 411 | 412 | :return: a dict with the keys: protocol, model, id, datatypes. 413 | """ 414 | protocol = create_string_buffer(20) 415 | model = create_string_buffer(20) 416 | sid = c_int() 417 | datatypes = c_int() 418 | 419 | self._lib.tdSensor(protocol, sizeof(protocol), model, sizeof(model), 420 | byref(sid), byref(datatypes)) 421 | return {'protocol': self._to_str(protocol), 422 | 'model': self._to_str(model), 423 | 'id': sid.value, 'datatypes': datatypes.value} 424 | 425 | def tdSensorValue(self, protocol, model, sid, datatype): 426 | """Get the sensor value for a given sensor. 427 | 428 | :return: a dict with the keys: value, timestamp. 429 | """ 430 | value = create_string_buffer(20) 431 | timestamp = c_int() 432 | 433 | self._lib.tdSensorValue(protocol, model, sid, datatype, 434 | value, sizeof(value), byref(timestamp)) 435 | return {'value': self._to_str(value), 'timestamp': timestamp.value} 436 | 437 | def tdController(self): 438 | """Get the next controller while iterating. 439 | 440 | :return: a dict with the keys: id, type, name, available. 441 | """ 442 | cid = c_int() 443 | ctype = c_int() 444 | name = create_string_buffer(255) 445 | available = c_int() 446 | 447 | self._lib.tdController(byref(cid), byref(ctype), name, sizeof(name), 448 | byref(available)) 449 | return {'id': cid.value, 'type': ctype.value, 450 | 'name': self._to_str(name), 'available': available.value} 451 | 452 | def tdControllerValue(self, cid, name): 453 | value = create_string_buffer(255) 454 | 455 | self._lib.tdControllerValue(cid, name, value, sizeof(value)) 456 | return self._to_str(value) 457 | -------------------------------------------------------------------------------- /tellcore/telldus.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012-2014 Erik Johansson 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License as 5 | # published by the Free Software Foundation; either version 3 of the 6 | # License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 16 | # USA 17 | 18 | try: 19 | import queue 20 | except ImportError: 21 | # Fall back on old (python 2) variant 22 | import Queue as queue 23 | 24 | import tellcore.constants as const 25 | from tellcore.library import Library, TelldusError, BaseCallbackDispatcher 26 | 27 | from datetime import datetime 28 | 29 | 30 | class QueuedCallbackDispatcher(BaseCallbackDispatcher): 31 | """The default callback dispatcher used by :class:`TelldusCore`. 32 | 33 | Queues callbacks that arrive from Telldus Core. Then calls them in the main 34 | thread (or more precise: the thread calling :func:`process_callback`) 35 | instead of the callback thread used by Telldus Core. This way the 36 | application using :class:`TelldusCore` don't have to do any thread 37 | synchronization. Only make sure :func:`process_pending_callbacks` is called 38 | regularly. 39 | """ 40 | 41 | def __init__(self): 42 | super(QueuedCallbackDispatcher, self).__init__() 43 | self._queue = queue.Queue() 44 | 45 | def on_callback(self, callback, *args): 46 | self._queue.put((callback, args)) 47 | 48 | def process_callback(self, block=True): 49 | """Dispatch a single callback in the current thread. 50 | 51 | :param boolean block: If True, blocks waiting for a callback to come. 52 | :return: True if a callback was processed; otherwise False. 53 | """ 54 | try: 55 | (callback, args) = self._queue.get(block=block) 56 | try: 57 | callback(*args) 58 | finally: 59 | self._queue.task_done() 60 | except queue.Empty: 61 | return False 62 | return True 63 | 64 | def process_pending_callbacks(self): 65 | """Dispatch all pending callbacks in the current thread.""" 66 | while self.process_callback(block=False): 67 | pass 68 | 69 | 70 | class AsyncioCallbackDispatcher(BaseCallbackDispatcher): 71 | """Dispatcher for use with the event loop available in Python 3.4+. 72 | 73 | Callbacks will be dispatched on the thread running the event loop. The loop 74 | argument should be a BaseEventLoop instance, e.g. the one returned from 75 | asyncio.get_event_loop(). 76 | """ 77 | def __init__(self, loop): 78 | super(AsyncioCallbackDispatcher, self).__init__() 79 | self._loop = loop 80 | 81 | def on_callback(self, callback, *args): 82 | self._loop.call_soon_threadsafe(callback, *args) 83 | 84 | 85 | class TelldusCore(object): 86 | """The main class for tellcore-py. 87 | 88 | Has methods for adding devices and for enumerating controllers, devices and 89 | sensors. Also handles callbacks; both registration and making sure the 90 | callbacks are processed in the main thread instead of the callback thread. 91 | """ 92 | 93 | def __init__(self, library_path=None, callback_dispatcher=None): 94 | """Create a new TelldusCore instance. 95 | 96 | Only one instance should be used per program. 97 | 98 | :param str library_path: Passed to the :class:`.library.Library` 99 | constructor. 100 | 101 | :param str callback_dispatcher: An instance implementing the 102 | :class:`.library.BaseCallbackDispatcher` interface ( 103 | e.g. :class:`QueuedCallbackDispatcher` or 104 | :class:`AsyncioCallbackDispatcher`) A callback dispatcher must be 105 | provided if callbacks are to be used. 106 | 107 | """ 108 | super(TelldusCore, self).__init__() 109 | self.lib = Library(library_path, callback_dispatcher) 110 | 111 | def __getattr__(self, name): 112 | if name == 'callback_dispatcher': 113 | return self.lib.callback_dispatcher 114 | raise AttributeError(name) 115 | 116 | def register_device_event(self, callback): 117 | """Register a new device event callback handler. 118 | 119 | See :ref:`event-example` for more information. 120 | 121 | :return: the callback id 122 | """ 123 | return self.lib.tdRegisterDeviceEvent(callback) 124 | 125 | def register_device_change_event(self, callback): 126 | """Register a new device change event callback handler. 127 | 128 | See :ref:`event-example` for more information. 129 | 130 | :return: the callback id 131 | """ 132 | return self.lib.tdRegisterDeviceChangeEvent(callback) 133 | 134 | def register_raw_device_event(self, callback): 135 | """Register a new raw device event callback handler. 136 | 137 | See :ref:`event-example` for more information. 138 | 139 | :return: the callback id 140 | """ 141 | return self.lib.tdRegisterRawDeviceEvent(callback) 142 | 143 | def register_sensor_event(self, callback): 144 | """Register a new sensor event callback handler. 145 | 146 | See :ref:`event-example` for more information. 147 | 148 | :return: the callback id 149 | """ 150 | return self.lib.tdRegisterSensorEvent(callback) 151 | 152 | def register_controller_event(self, callback): 153 | """Register a new controller event callback handler. 154 | 155 | See :ref:`event-example` for more information. 156 | 157 | :return: the callback id 158 | """ 159 | return self.lib.tdRegisterControllerEvent(callback) 160 | 161 | def unregister_callback(self, cid): 162 | """Unregister a callback handler. 163 | 164 | :param int id: the callback id as returned from one of the 165 | register_*_event methods. 166 | """ 167 | self.lib.tdUnregisterCallback(cid) 168 | 169 | def devices(self): 170 | """Return all known devices. 171 | 172 | :return: list of :class:`Device` or :class:`DeviceGroup` instances. 173 | """ 174 | devices = [] 175 | count = self.lib.tdGetNumberOfDevices() 176 | for i in range(count): 177 | device = DeviceFactory(self.lib.tdGetDeviceId(i), lib=self.lib) 178 | devices.append(device) 179 | return devices 180 | 181 | def sensors(self): 182 | """Return all known sensors. 183 | 184 | :return: list of :class:`Sensor` instances. 185 | """ 186 | sensors = [] 187 | try: 188 | while True: 189 | sensor = self.lib.tdSensor() 190 | sensors.append(Sensor(lib=self.lib, **sensor)) 191 | except TelldusError as e: 192 | if e.error != const.TELLSTICK_ERROR_DEVICE_NOT_FOUND: 193 | raise 194 | return sensors 195 | 196 | def controllers(self): 197 | """Return all known controllers. 198 | 199 | Requires Telldus core library version >= 2.1.2. 200 | 201 | :return: list of :class:`Controller` instances. 202 | """ 203 | controllers = [] 204 | try: 205 | while True: 206 | controller = self.lib.tdController() 207 | del controller["name"] 208 | del controller["available"] 209 | controllers.append(Controller(lib=self.lib, **controller)) 210 | except TelldusError as e: 211 | if e.error != const.TELLSTICK_ERROR_NOT_FOUND: 212 | raise 213 | return controllers 214 | 215 | def add_device(self, name, protocol, model=None, **parameters): 216 | """Add a new device. 217 | 218 | :return: a :class:`Device` or :class:`DeviceGroup` instance. 219 | """ 220 | device = Device(self.lib.tdAddDevice(), lib=self.lib) 221 | try: 222 | device.name = name 223 | device.protocol = protocol 224 | if model: 225 | device.model = model 226 | for key, value in parameters.items(): 227 | device.set_parameter(key, value) 228 | 229 | # Return correct type 230 | return DeviceFactory(device.id, lib=self.lib) 231 | except Exception: 232 | import sys 233 | exc_info = sys.exc_info() 234 | try: 235 | device.remove() 236 | except: 237 | pass 238 | 239 | if "with_traceback" in dir(Exception): 240 | raise exc_info[0].with_traceback(exc_info[1], exc_info[2]) 241 | else: 242 | exec("raise exc_info[0], exc_info[1], exc_info[2]") 243 | 244 | def add_group(self, name, devices): 245 | """Add a new device group. 246 | 247 | :return: a :class:`DeviceGroup` instance. 248 | """ 249 | device = self.add_device(name, "group") 250 | device.add_to_group(devices) 251 | return device 252 | 253 | def send_raw_command(self, command, reserved=0): 254 | """Send a raw command.""" 255 | return self.lib.tdSendRawCommand(command, reserved) 256 | 257 | def connect_controller(self, vid, pid, serial): 258 | """Connect a controller.""" 259 | self.lib.tdConnectTellStickController(vid, pid, serial) 260 | 261 | def disconnect_controller(self, vid, pid, serial): 262 | """Disconnect a controller.""" 263 | self.lib.tdDisconnectTellStickController(vid, pid, serial) 264 | 265 | 266 | def DeviceFactory(id, lib=None): 267 | """Create the correct device instance based on device type and return it. 268 | 269 | :return: a :class:`Device` or :class:`DeviceGroup` instance. 270 | """ 271 | lib = lib or Library() 272 | if lib.tdGetDeviceType(id) == const.TELLSTICK_TYPE_GROUP: 273 | return DeviceGroup(id, lib=lib) 274 | return Device(id, lib=lib) 275 | 276 | 277 | class Device(object): 278 | """A device that can be controlled by Telldus Core. 279 | 280 | Can be instantiated directly if the id is known, but using 281 | :func:`DeviceFactory` is recommended. Otherwise returned from 282 | :func:`TelldusCore.add_device` or :func:`TelldusCore.devices`. 283 | """ 284 | 285 | PARAMETERS = ["devices", "house", "unit", "code", "system", "units", 286 | "fade"] 287 | 288 | def __init__(self, id, lib=None): 289 | super(Device, self).__init__() 290 | 291 | lib = lib or Library() 292 | super(Device, self).__setattr__('id', id) 293 | super(Device, self).__setattr__('lib', lib) 294 | 295 | def remove(self): 296 | """Remove the device from Telldus Core.""" 297 | return self.lib.tdRemoveDevice(self.id) 298 | 299 | def __getattr__(self, name): 300 | if name == 'name': 301 | func = self.lib.tdGetName 302 | elif name == 'protocol': 303 | func = self.lib.tdGetProtocol 304 | elif name == 'model': 305 | func = self.lib.tdGetModel 306 | elif name == 'type': 307 | func = self.lib.tdGetDeviceType 308 | else: 309 | raise AttributeError(name) 310 | return func(self.id) 311 | 312 | def __setattr__(self, name, value): 313 | if name == 'name': 314 | func = self.lib.tdSetName 315 | elif name == 'protocol': 316 | func = self.lib.tdSetProtocol 317 | elif name == 'model': 318 | func = self.lib.tdSetModel 319 | else: 320 | raise AttributeError(name) 321 | func(self.id, value) 322 | 323 | def parameters(self): 324 | """Get dict with all set parameters.""" 325 | parameters = {} 326 | for name in self.PARAMETERS: 327 | try: 328 | parameters[name] = self.get_parameter(name) 329 | except AttributeError: 330 | pass 331 | return parameters 332 | 333 | def get_parameter(self, name): 334 | """Get a parameter.""" 335 | default_value = "$%!)(INVALID)(!%$" 336 | value = self.lib.tdGetDeviceParameter(self.id, name, default_value) 337 | if value == default_value: 338 | raise AttributeError(name) 339 | return value 340 | 341 | def set_parameter(self, name, value): 342 | """Set a parameter.""" 343 | self.lib.tdSetDeviceParameter(self.id, name, str(value)) 344 | 345 | def turn_on(self): 346 | """Turn on the device.""" 347 | self.lib.tdTurnOn(self.id) 348 | 349 | def turn_off(self): 350 | """Turn off the device.""" 351 | self.lib.tdTurnOff(self.id) 352 | 353 | def bell(self): 354 | """Send "bell" command to the device.""" 355 | self.lib.tdBell(self.id) 356 | 357 | def dim(self, level): 358 | """Dim the device. 359 | 360 | :param int level: The level to dim to in the range [0, 255]. 361 | """ 362 | self.lib.tdDim(self.id, level) 363 | 364 | def execute(self): 365 | """Send "execute" command to the device.""" 366 | self.lib.tdExecute(self.id) 367 | 368 | def up(self): 369 | """Send "up" command to the device.""" 370 | self.lib.tdUp(self.id) 371 | 372 | def down(self): 373 | """Send "down" command to the device.""" 374 | self.lib.tdDown(self.id) 375 | 376 | def stop(self): 377 | """Send "stop" command to the device.""" 378 | self.lib.tdStop(self.id) 379 | 380 | def learn(self): 381 | """Send "learn" command to the device.""" 382 | self.lib.tdLearn(self.id) 383 | 384 | def methods(self, methods_supported): 385 | """Query the device for supported methods.""" 386 | return self.lib.tdMethods(self.id, methods_supported) 387 | 388 | def last_sent_command(self, methods_supported): 389 | """Get the last sent (or seen) command.""" 390 | return self.lib.tdLastSentCommand(self.id, methods_supported) 391 | 392 | def last_sent_value(self): 393 | """Get the last sent (or seen) value.""" 394 | try: 395 | return int(self.lib.tdLastSentValue(self.id)) 396 | except ValueError: 397 | return None 398 | 399 | 400 | class DeviceGroup(Device): 401 | """Extends :class:`Device` with methods for managing a group 402 | 403 | E.g. when a group is turned on, all devices in that group are turned on. 404 | """ 405 | 406 | def add_to_group(self, devices): 407 | """Add device(s) to the group.""" 408 | ids = {d.id for d in self.devices_in_group()} 409 | ids.update(self._device_ids(devices)) 410 | self._set_group(ids) 411 | 412 | def remove_from_group(self, devices): 413 | """Remove device(s) from the group.""" 414 | ids = {d.id for d in self.devices_in_group()} 415 | ids.difference_update(self._device_ids(devices)) 416 | self._set_group(ids) 417 | 418 | def devices_in_group(self): 419 | """Fetch list of devices in group.""" 420 | try: 421 | devices = self.get_parameter('devices') 422 | except AttributeError: 423 | return [] 424 | 425 | ctor = DeviceFactory 426 | return [ctor(int(x), lib=self.lib) for x in devices.split(',') if x] 427 | 428 | @staticmethod 429 | def _device_ids(devices): 430 | try: 431 | iter(devices) 432 | except TypeError: 433 | devices = [devices] 434 | 435 | ids = set() 436 | for device in devices: 437 | try: 438 | ids.add(device.id) 439 | except AttributeError: 440 | # Assume device is id 441 | ids.add(int(device)) 442 | return ids 443 | 444 | def _set_group(self, ids): 445 | self.set_parameter('devices', ','.join([str(x) for x in ids])) 446 | 447 | 448 | class Sensor(object): 449 | """Represents a sensor. 450 | 451 | Returned from :func:`TelldusCore.sensors` 452 | """ 453 | 454 | DATATYPES = {"temperature": const.TELLSTICK_TEMPERATURE, 455 | "humidity": const.TELLSTICK_HUMIDITY, 456 | "rainrate": const.TELLSTICK_RAINRATE, 457 | "raintotal": const.TELLSTICK_RAINTOTAL, 458 | "winddirection": const.TELLSTICK_WINDDIRECTION, 459 | "windaverage": const.TELLSTICK_WINDAVERAGE, 460 | "windgust": const.TELLSTICK_WINDGUST} 461 | 462 | def __init__(self, protocol, model, id, datatypes, lib=None): 463 | super(Sensor, self).__init__() 464 | self.protocol = protocol 465 | self.model = model 466 | self.id = id 467 | self.datatypes = datatypes 468 | self.lib = lib or Library() 469 | 470 | def has_value(self, datatype): 471 | """Return True if the sensor supports the given data type. 472 | 473 | sensor.has_value(TELLSTICK_TEMPERATURE) is identical to calling 474 | sensor.has_temperature(). 475 | """ 476 | return (self.datatypes & datatype) != 0 477 | 478 | def value(self, datatype): 479 | """Return the :class:`SensorValue` for the given data type. 480 | 481 | sensor.value(TELLSTICK_TEMPERATURE) is identical to calling 482 | sensor.temperature(). 483 | """ 484 | value = self.lib.tdSensorValue( 485 | self.protocol, self.model, self.id, datatype) 486 | return SensorValue(datatype, value['value'], value['timestamp']) 487 | 488 | def __getattr__(self, name): 489 | typename = name.replace("has_", "", 1) 490 | if typename in Sensor.DATATYPES: 491 | datatype = Sensor.DATATYPES[typename] 492 | if name == typename: 493 | return lambda: self.value(datatype) 494 | else: 495 | return lambda: self.has_value(datatype) 496 | raise AttributeError(name) 497 | 498 | 499 | class SensorValue(object): 500 | """Represents a single sensor value. 501 | 502 | Returned from :func:`Sensor.value`. 503 | """ 504 | 505 | def __init__(self, datatype, value, timestamp): 506 | super(SensorValue, self).__init__() 507 | self.datatype = datatype 508 | self.value = value 509 | self.timestamp = timestamp 510 | 511 | def __getattr__(self, name): 512 | if name == "datetime": 513 | return datetime.fromtimestamp(self.timestamp) 514 | raise AttributeError(name) 515 | 516 | 517 | class Controller(object): 518 | """Represents a Telldus controller. 519 | 520 | Returned from :func:`TelldusCore.controllers` 521 | """ 522 | 523 | def __init__(self, id, type, lib=None): 524 | lib = lib or Library() 525 | 526 | super(Controller, self).__init__() 527 | super(Controller, self).__setattr__('id', id) 528 | super(Controller, self).__setattr__('type', type) 529 | super(Controller, self).__setattr__('lib', lib) 530 | 531 | def __getattr__(self, name): 532 | try: 533 | return self.lib.tdControllerValue(self.id, name) 534 | except TelldusError as e: 535 | if e.error == const.TELLSTICK_ERROR_METHOD_NOT_SUPPORTED: 536 | raise AttributeError(name) 537 | raise 538 | 539 | def __setattr__(self, name, value): 540 | try: 541 | self.lib.tdSetControllerValue(self.id, name, value) 542 | except TelldusError as e: 543 | if e.error == const.TELLSTICK_ERROR_SYNTAX: 544 | raise AttributeError(name) 545 | raise 546 | -------------------------------------------------------------------------------- /tests/mocklib.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012-2014 Erik Johansson 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License as 5 | # published by the Free Software Foundation; either version 3 of the 6 | # License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 16 | # USA 17 | 18 | try: 19 | import queue 20 | except ImportError: 21 | import Queue as queue 22 | 23 | import ctypes 24 | import threading 25 | 26 | import tellcore.library 27 | 28 | ByRefArgType = type(ctypes.byref(ctypes.c_int(0))) 29 | 30 | 31 | class MockLibLoader(object): 32 | def __init__(self, mocklib): 33 | object.__init__(self) 34 | self.load_count = 0 35 | self.mocklib = mocklib 36 | 37 | def LoadLibrary(self, name): 38 | self.load_count += 1 39 | return self.mocklib 40 | 41 | 42 | class MockTelldusCoreLib(object): 43 | def __init__(self): 44 | object.__init__(self) 45 | 46 | self.tdInit = lambda: None 47 | self.tdClose = lambda: None 48 | 49 | self.tdReleaseString = lambda x: None 50 | self.tdGetErrorString = lambda x: None 51 | 52 | def __getattr__(self, name): 53 | if name in tellcore.library.Library._functions: 54 | func = MockCFunction(name, self) 55 | object.__setattr__(self, name, func) 56 | return func 57 | raise AttributeError(name) 58 | 59 | def __setattr__(self, name, value): 60 | if name in tellcore.library.Library._functions: 61 | func = getattr(self, name) 62 | func.implementation = value 63 | else: 64 | object.__setattr__(self, name, value) 65 | 66 | 67 | class MockCFunction(object): 68 | def __init__(self, name, lib): 69 | object.__init__(self) 70 | self.name = name 71 | self.lib = lib 72 | self.implementation = None 73 | self.restype = None 74 | self.argtypes = None 75 | self.errcheck = None 76 | 77 | def __call__(self, *args): 78 | if self.implementation is None: 79 | raise NotImplementedError("%s is not implemented" % self.name) 80 | 81 | if self.argtypes is None: 82 | raise NotImplementedError("%s not configured" % self.name) 83 | 84 | if len(self.argtypes) != len(args): 85 | raise TypeError("%s() takes exactly %d argument(s) (%d given)" % 86 | (self.name, len(self.argument), len(args))) 87 | 88 | c_args = [] 89 | 90 | # Verify that the arguments are of correct type 91 | for c_type, value in zip(self.argtypes, args): 92 | c_args.append(value) 93 | if type(value) is not c_type: 94 | # The 'raw' attribute is the pointer for string buffers 95 | if hasattr(value, 'raw'): 96 | c_type.from_param(value.raw) 97 | elif type(value) is not ByRefArgType: 98 | c_type.from_param(value) 99 | # Pass the possibly converted value instead of the original 100 | c_args[-1] = c_type(value).value 101 | 102 | res = self.implementation(*c_args) 103 | 104 | # Functions returning char pointers are set up to return the pointer as 105 | # a void pointer. Do the conversion here to match the real thing. 106 | if self.restype is ctypes.c_void_p: 107 | res = ctypes.cast(res, ctypes.c_void_p) 108 | # For the rest, verify that the return value is of correct type. 109 | elif self.restype is not None: 110 | self.restype.from_param(res) 111 | 112 | if self.errcheck is not None: 113 | res = self.errcheck(res, self, args) 114 | return res 115 | 116 | 117 | class MockEventDispatcher(threading.Thread): 118 | def __init__(self): 119 | threading.Thread.__init__(self, name="MockEventDispatcher") 120 | 121 | self.next_callback_id = 1 122 | 123 | # callback id -> callback object 124 | self.callbacks = {} 125 | 126 | # Lists of callback ids 127 | self.device_callbacks = [] 128 | self.device_change_callbacks = [] 129 | self.raw_device_callbacks = [] 130 | self.sensor_callbacks = [] 131 | self.controller_callbacks = [] 132 | 133 | self.queue = queue.Queue() 134 | self.running = True 135 | self.start() 136 | 137 | def setup_lib_functions(self, mocklib): 138 | def tdUnregisterCallback(cid): 139 | del self.callbacks[cid] 140 | return 1 141 | mocklib.tdUnregisterCallback = tdUnregisterCallback 142 | 143 | def register_callback(active_list, callback, context): 144 | cid = self.next_callback_id 145 | self.next_callback_id += 1 146 | 147 | self.callbacks[cid] = (callback, context) 148 | active_list.append(cid) 149 | return cid 150 | 151 | callback_types = { 152 | "tdRegisterDeviceEvent": self.device_callbacks, 153 | "tdRegisterDeviceChangeEvent": self.device_change_callbacks, 154 | "tdRegisterRawDeviceEvent": self.raw_device_callbacks, 155 | "tdRegisterSensorEvent": self.sensor_callbacks, 156 | "tdRegisterControllerEvent": self.controller_callbacks 157 | } 158 | 159 | for name, id_list in callback_types.items(): 160 | func = lambda callback, context, id_list=id_list: \ 161 | register_callback(id_list, callback, context) 162 | setattr(mocklib, name, func) 163 | 164 | def run(self): 165 | while self.running: 166 | (callback, args) = self.queue.get() 167 | try: 168 | callback(*args) 169 | except: 170 | pass 171 | finally: 172 | self.queue.task_done() 173 | 174 | def stop(self): 175 | assert self.running 176 | 177 | def stop_thread(): 178 | self.running = False 179 | self.queue_event(stop_thread) 180 | self.queue.join() 181 | 182 | def queue_event(self, callback, *args): 183 | assert self.running 184 | self.queue.put((callback, args)) 185 | 186 | def _trigger_event(self, callback_list, *args): 187 | for cid in callback_list: 188 | if cid in self.callbacks: 189 | (callback, context) = self.callbacks[cid] 190 | event_args = args + (cid, context) 191 | self.queue_event(callback, *event_args) 192 | # Make sure all events are delivered 193 | self.queue.join() 194 | 195 | def trigger_device_event(self, device_id, method, data): 196 | self._trigger_event(self.device_callbacks, device_id, method, data) 197 | 198 | def trigger_device_change_event(self, device_id, event, type_): 199 | self._trigger_event(self.device_change_callbacks, device_id, 200 | event, type_) 201 | 202 | def trigger_raw_device_event(self, data, controller_id): 203 | self._trigger_event(self.raw_device_callbacks, data, controller_id) 204 | 205 | def trigger_sensor_event(self, protocol, model, sensor_id, dataType, value, 206 | timestamp): 207 | self._trigger_event(self.sensor_callbacks, protocol, model, sensor_id, 208 | dataType, value, timestamp) 209 | 210 | def trigger_controller_event(self, controller_id, event, type_, new_value): 211 | self._trigger_event(self.controller_callbacks, controller_id, event, 212 | type_, new_value) 213 | -------------------------------------------------------------------------------- /tests/test_library.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012-2014 Erik Johansson 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License as 5 | # published by the Free Software Foundation; either version 3 of the 6 | # License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 16 | # USA 17 | 18 | import unittest 19 | 20 | import tellcore.library 21 | from tellcore.constants import * 22 | 23 | import ctypes 24 | import gc 25 | import mocklib 26 | 27 | Library = tellcore.library.Library 28 | TelldusError = tellcore.library.TelldusError 29 | 30 | 31 | class Test(unittest.TestCase): 32 | def setUp(self): 33 | self.initialized = False 34 | self.mocklib = mocklib.MockTelldusCoreLib() 35 | 36 | def tdInit(): 37 | if self.initialized: 38 | raise RuntimeError("already initialized") 39 | self.initialized = True 40 | self.mocklib.tdInit = tdInit 41 | 42 | def tdClose(): 43 | if not self.initialized: 44 | raise RuntimeError("not initialized") 45 | self.initialized = False 46 | self.mocklib.tdClose = tdClose 47 | 48 | self.loader = mocklib.MockLibLoader(self.mocklib) 49 | tellcore.library.DllLoader = self.loader 50 | 51 | def tearDown(self): 52 | gc.collect() 53 | 54 | def test_libloader(self): 55 | self.assertEqual(self.loader.load_count, 0) 56 | lib = Library() 57 | self.assertEqual(self.loader.load_count, 1) 58 | self.assertEqual(lib._lib, self.mocklib) 59 | 60 | def test_oneinstance(self): 61 | """Test that the singleton works""" 62 | lib1 = Library() 63 | lib2 = Library() 64 | self.assertEqual(self.loader.load_count, 1) 65 | 66 | del lib1, lib2 67 | gc.collect() 68 | 69 | lib = Library() 70 | self.assertEqual(self.loader.load_count, 2) 71 | self.assertIsInstance(lib, Library) 72 | 73 | def test_initialized(self): 74 | self.assertFalse(self.initialized) 75 | lib1 = Library() 76 | self.assertTrue(self.initialized) 77 | lib2 = Library() 78 | self.assertTrue(self.initialized) 79 | 80 | del lib1 81 | gc.collect() 82 | self.assertTrue(self.initialized) 83 | 84 | del lib2 85 | gc.collect() 86 | self.assertFalse(self.initialized) 87 | 88 | def test_private_methods(self): 89 | lib = Library() 90 | self.assertRaises(NotImplementedError, lib.tdInit) 91 | self.assertRaises(NotImplementedError, lib.tdClose) 92 | self.assertRaises(NotImplementedError, lib.tdReleaseString, 0) 93 | 94 | def test_string_releaser(self): 95 | """Test that all strings returned from core lib are released""" 96 | released = [] 97 | 98 | def tdReleaseString(pointer): 99 | released.append(pointer.value) 100 | self.mocklib.tdReleaseString = tdReleaseString 101 | 102 | returned = [] 103 | 104 | def tdGetErrorString(error): 105 | string = ctypes.c_char_p( 106 | ("error %d" % error).encode(Library.STRING_ENCODING)) 107 | void_pointer = ctypes.cast(string, ctypes.c_void_p) 108 | returned.append(void_pointer.value) 109 | return string 110 | self.mocklib.tdGetErrorString = tdGetErrorString 111 | 112 | lib = Library() 113 | for i in range(-5, 0): 114 | lib.tdGetErrorString(i) 115 | 116 | self.assertEqual(len(released), 5) 117 | self.assertEqual(returned, released) 118 | 119 | def test_string_parameter(self): 120 | def int_strings(ret, ignore, string1, string2="defaultValue"): 121 | self.assertEqual(string1, b"aString") 122 | if string2 != "defaultValue": 123 | self.assertEqual(string2, b"aSecondString") 124 | return ret 125 | 126 | string_int = lambda s, i: int_strings(0, i, s) 127 | ints_string = lambda i1, i2, s: int_strings(0, i1, s) 128 | strings_ignore = lambda s1, s2, *args: int_strings(0, 0, s1, s2) 129 | int_string_ignore = lambda i, s, *args: int_strings(0, i, s) 130 | 131 | self.mocklib.tdSetName = lambda *a: int_strings(True, *a) 132 | self.mocklib.tdSetProtocol = lambda *a: int_strings(True, *a) 133 | self.mocklib.tdSetModel = lambda *a: int_strings(True, *a) 134 | self.mocklib.tdSetDeviceParameter = lambda *a: int_strings(True, *a) 135 | self.mocklib.tdGetDeviceParameter = lambda *a: int_strings(0, *a) 136 | self.mocklib.tdSendRawCommand = string_int 137 | self.mocklib.tdConnectTellStickController = ints_string 138 | self.mocklib.tdDisconnectTellStickController = ints_string 139 | self.mocklib.tdSensorValue = strings_ignore 140 | self.mocklib.tdControllerValue = int_string_ignore 141 | self.mocklib.tdSetControllerValue = lambda *a: int_strings(0, *a) 142 | 143 | lib = Library() 144 | 145 | lib.tdSetName(1, "aString") 146 | lib.tdSetProtocol(1, "aString") 147 | lib.tdSetModel(1, "aString") 148 | lib.tdSetDeviceParameter(1, "aString", "aSecondString") 149 | lib.tdGetDeviceParameter(1, "aString", "aSecondString") 150 | lib.tdSendRawCommand("aString", 1) 151 | lib.tdConnectTellStickController(0, 0, "aString") 152 | lib.tdDisconnectTellStickController(0, 0, "aString") 153 | lib.tdSensorValue("aString", "aSecondString", 0, 0) 154 | lib.tdControllerValue(1, "aString") 155 | lib.tdSetControllerValue(1, "aString", "aSecondString") 156 | 157 | def test_unicode(self): 158 | test_name = b'\xc3\xa5\xc3\xa4\xc3\xb6' 159 | 160 | def tdSetName(id, name): 161 | self.assertIs(type(name), bytes) 162 | self.assertEqual(name, test_name) 163 | return True 164 | self.mocklib.tdSetName = tdSetName 165 | 166 | lib = Library() 167 | 168 | lib.tdSetName(1, test_name.decode(Library.STRING_ENCODING)) 169 | 170 | def setup_callback(self, registered_ids, unregistered_ids): 171 | def tdRegisterEvent(*args): 172 | cid = len(registered_ids) + 1 173 | registered_ids.append(cid) 174 | return cid 175 | self.mocklib.tdRegisterDeviceEvent = tdRegisterEvent 176 | self.mocklib.tdRegisterDeviceChangeEvent = tdRegisterEvent 177 | self.mocklib.tdRegisterRawDeviceEvent = tdRegisterEvent 178 | self.mocklib.tdRegisterSensorEvent = tdRegisterEvent 179 | self.mocklib.tdRegisterControllerEvent = tdRegisterEvent 180 | 181 | def tdUnregisterCallback(cid): 182 | unregistered_ids.append(cid) 183 | return TELLSTICK_SUCCESS 184 | self.mocklib.tdUnregisterCallback = tdUnregisterCallback 185 | 186 | def test_callback_cleanup(self): 187 | registered_ids = [] 188 | unregistered_ids = [] 189 | self.setup_callback(registered_ids, unregistered_ids) 190 | 191 | def callback(*args): 192 | pass 193 | 194 | dispatcher = tellcore.library.DirectCallbackDispatcher 195 | lib = Library(callback_dispatcher=dispatcher) 196 | lib.tdRegisterDeviceEvent(callback) 197 | lib.tdRegisterDeviceChangeEvent(callback) 198 | lib.tdRegisterRawDeviceEvent(callback) 199 | lib.tdRegisterSensorEvent(callback) 200 | lib.tdRegisterControllerEvent(callback) 201 | 202 | self.assertEqual(len(registered_ids), 5) 203 | self.assertEqual(len(unregistered_ids), 0) 204 | 205 | lib = None 206 | gc.collect() 207 | self.assertEqual(registered_ids, unregistered_ids) 208 | 209 | def test_callback_not_shared(self): 210 | registered_ids = [] 211 | unregistered_ids = [] 212 | self.setup_callback(registered_ids, unregistered_ids) 213 | 214 | def callback(*args): 215 | pass 216 | 217 | dispatcher = tellcore.library.DirectCallbackDispatcher 218 | lib = Library(callback_dispatcher=dispatcher) 219 | 220 | cid = lib.tdRegisterDeviceEvent(callback) 221 | self.assertEqual(registered_ids, [cid]) 222 | self.assertEqual(unregistered_ids, []) 223 | 224 | lib_copy = Library(callback_dispatcher=dispatcher) 225 | 226 | cid_copy = lib_copy.tdRegisterDeviceEvent(callback) 227 | self.assertEqual(registered_ids, [cid, cid_copy]) 228 | self.assertEqual(unregistered_ids, []) 229 | 230 | lib = None 231 | gc.collect() 232 | self.assertEqual(unregistered_ids, [cid]) 233 | 234 | lib_copy = None 235 | gc.collect() 236 | self.assertEqual(registered_ids, unregistered_ids) 237 | 238 | def test_exception_on_error(self): 239 | def tdGetNumberOfDevices(): 240 | return TELLSTICK_ERROR_CONNECTING_SERVICE 241 | self.mocklib.tdGetNumberOfDevices = tdGetNumberOfDevices 242 | 243 | lib = Library() 244 | with self.assertRaises(TelldusError) as cm: 245 | lib.tdGetNumberOfDevices() 246 | self.assertEqual(cm.exception.error, 247 | TELLSTICK_ERROR_CONNECTING_SERVICE) 248 | 249 | if __name__ == '__main__': 250 | unittest.main() 251 | -------------------------------------------------------------------------------- /tests/test_telldus.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012-2014 Erik Johansson 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License as 5 | # published by the Free Software Foundation; either version 3 of the 6 | # License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, but 9 | # WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 16 | # USA 17 | 18 | import unittest 19 | 20 | from tellcore.telldus import TelldusCore, TelldusError, Device, DeviceGroup, \ 21 | QueuedCallbackDispatcher 22 | from tellcore.constants import * 23 | import tellcore.library 24 | 25 | from ctypes import c_char_p, c_int 26 | import gc 27 | import mocklib 28 | 29 | 30 | class Test(unittest.TestCase): 31 | def setUp(self): 32 | self.mocklib = mocklib.MockTelldusCoreLib() 33 | self.mockdispatcher = mocklib.MockEventDispatcher() 34 | self.mockdispatcher.setup_lib_functions(self.mocklib) 35 | 36 | self.loader = mocklib.MockLibLoader(self.mocklib) 37 | tellcore.library.DllLoader = self.loader 38 | 39 | def tearDown(self): 40 | self.mockdispatcher.stop() 41 | del self.mockdispatcher 42 | 43 | tellcore.library.DllLoader = None 44 | del self.loader 45 | del self.mocklib 46 | 47 | gc.collect() 48 | 49 | # If a test case fails and throws an exception, the TelldusCore 50 | # instance will remain in the frame included in the exception which 51 | # will stop the library from being unloaded. This means that the next 52 | # test case will reuse the same library instance which causes it to 53 | # fail. 54 | if tellcore.library.Library._lib: 55 | tellcore.library.Library._lib = None 56 | tellcore.library.Library._refcount = 0 57 | 58 | def decode_string(self, string): 59 | return string.decode(tellcore.library.Library.STRING_ENCODING) 60 | 61 | def encode_string(self, string): 62 | return string.encode(tellcore.library.Library.STRING_ENCODING) 63 | 64 | def event_tester(self, core, registrator, trigger, trigger_args): 65 | event_args = {} 66 | 67 | def callback(*args): 68 | # Strip away callback id 69 | event_args[args[-1]] = args[:-1] 70 | 71 | id1 = registrator(callback) 72 | id2 = registrator(callback) 73 | id3 = registrator(callback) 74 | id4 = registrator(callback) 75 | core.unregister_callback(id4) 76 | core.unregister_callback(id2) 77 | 78 | trigger(*trigger_args) 79 | core.callback_dispatcher.process_pending_callbacks() 80 | 81 | callback_args = [] 82 | for arg in trigger_args: 83 | if type(arg.value) is bytes: 84 | callback_args.append(self.decode_string(arg.value)) 85 | else: 86 | callback_args.append(arg.value) 87 | callback_args = tuple(callback_args) 88 | 89 | self.assertEqual(event_args, {id1: callback_args, id3: callback_args}) 90 | 91 | def test_device_event(self): 92 | core = TelldusCore(callback_dispatcher=QueuedCallbackDispatcher()) 93 | self.event_tester(core, core.register_device_event, 94 | self.mockdispatcher.trigger_device_event, 95 | (c_int(1), c_int(2), c_char_p(b"foo"))) 96 | 97 | def test_device_change_event(self): 98 | core = TelldusCore(callback_dispatcher=QueuedCallbackDispatcher()) 99 | self.event_tester(core, core.register_device_change_event, 100 | self.mockdispatcher.trigger_device_change_event, 101 | (c_int(3), c_int(4), c_int(5))) 102 | 103 | def test_raw_device_event(self): 104 | core = TelldusCore(callback_dispatcher=QueuedCallbackDispatcher()) 105 | self.event_tester(core, core.register_raw_device_event, 106 | self.mockdispatcher.trigger_raw_device_event, 107 | (c_char_p(b"bar"), c_int(6))) 108 | 109 | def test_sensor_event(self): 110 | core = TelldusCore(callback_dispatcher=QueuedCallbackDispatcher()) 111 | self.event_tester(core, core.register_sensor_event, 112 | self.mockdispatcher.trigger_sensor_event, 113 | (c_char_p(b"proto"), c_char_p(b"model"), c_int(7), 114 | c_int(8), c_char_p(b"value"), c_int(9))) 115 | 116 | def test_controller_event(self): 117 | core = TelldusCore(callback_dispatcher=QueuedCallbackDispatcher()) 118 | self.event_tester(core, core.register_controller_event, 119 | self.mockdispatcher.trigger_controller_event, 120 | (c_int(10), c_int(11), c_int(12), c_char_p(b"new"))) 121 | 122 | def test_group(self): 123 | self.mocklib.tdAddDevice = lambda: 1 124 | self.mocklib.tdGetDeviceType = lambda id: TELLSTICK_TYPE_GROUP 125 | 126 | device = {} 127 | device['parameters'] = {} 128 | 129 | def tdSetName(id, name): 130 | device['name'] = self.decode_string(name) 131 | return id == 1 132 | self.mocklib.tdSetName = tdSetName 133 | 134 | def tdSetProtocol(id, protocol): 135 | device['protocol'] = self.decode_string(protocol) 136 | return id == 1 137 | self.mocklib.tdSetProtocol = tdSetProtocol 138 | 139 | def tdSetDeviceParameter(id, name, value): 140 | name = self.decode_string(name) 141 | if value: 142 | device['parameters'][name] = self.decode_string(value) 143 | elif name in device['parameters']: 144 | del device['parameters'][name] 145 | return id == 1 146 | self.mocklib.tdSetDeviceParameter = tdSetDeviceParameter 147 | 148 | def tdGetDeviceParameter(id, name, default): 149 | name = self.decode_string(name) 150 | try: 151 | return c_char_p(self.encode_string(device['parameters'][name])) 152 | except KeyError: 153 | return c_char_p(default) 154 | self.mocklib.tdGetDeviceParameter = tdGetDeviceParameter 155 | 156 | core = TelldusCore() 157 | dev = core.add_group("test", [1, Device(2), 3, 4]) 158 | 159 | self.assertEqual("test", device['name']) 160 | self.assertEqual("group", device['protocol']) 161 | self.assertDictEqual({'devices': '1,2,3,4'}, device['parameters']) 162 | 163 | dev.add_to_group(5) 164 | self.assertDictEqual({'devices': '1,2,3,4,5'}, device['parameters']) 165 | 166 | dev.remove_from_group([3, 2, 6, Device(2), Device(1)]) 167 | self.assertDictEqual({'devices': '4,5'}, device['parameters']) 168 | self.assertListEqual([4, 5], [d.id for d in dev.devices_in_group()]) 169 | 170 | dev.remove_from_group(4) 171 | self.assertDictEqual({'devices': '5'}, device['parameters']) 172 | self.assertIsInstance(dev.devices_in_group()[0], DeviceGroup) 173 | 174 | dev.remove_from_group([5]) 175 | self.assertDictEqual({}, device['parameters']) 176 | 177 | def test_devices(self): 178 | devs = {0: {'protocol': b"proto_1", 'model': b"model_1"}, 179 | 3: {'protocol': b"proto_2", 'model': b"model_2"}, 180 | 6: {'protocol': b"proto_3", 'model': b"model_3"}} 181 | 182 | self.mocklib.tdGetNumberOfDevices = lambda: len(devs) 183 | self.mocklib.tdGetDeviceId = lambda index: index * 3 184 | self.mocklib.tdGetDeviceType = lambda id: TELLSTICK_TYPE_DEVICE 185 | self.mocklib.tdGetProtocol = lambda id: \ 186 | c_char_p(devs[id]['protocol']) 187 | self.mocklib.tdGetModel = lambda id: \ 188 | c_char_p(devs[id]['model']) 189 | 190 | core = TelldusCore() 191 | devices = core.devices() 192 | 193 | self.assertEqual(3, len(devices)) 194 | self.assertEqual(['proto_1', 'proto_2', 'proto_3'], 195 | [d.protocol for d in devices]) 196 | self.assertEqual(['model_1', 'model_2', 'model_3'], 197 | [d.model for d in devices]) 198 | 199 | def test_device(self): 200 | def actor(id): 201 | if id == 3: 202 | return TELLSTICK_SUCCESS 203 | else: 204 | return TELLSTICK_ERROR_DEVICE_NOT_FOUND 205 | self.mocklib.tdTurnOn = actor 206 | self.mocklib.tdTurnOff = actor 207 | self.mocklib.tdBell = lambda id: TELLSTICK_ERROR_METHOD_NOT_SUPPORTED 208 | 209 | device = Device(3) 210 | device.turn_on() 211 | device.turn_off() 212 | self.assertRaises(TelldusError, device.bell) 213 | 214 | device = Device(4) 215 | self.assertRaises(TelldusError, device.turn_on) 216 | self.assertRaises(TelldusError, device.turn_off) 217 | self.assertRaises(TelldusError, device.bell) 218 | 219 | def test_sensors(self): 220 | self.sensor_index = 0 221 | 222 | def tdSensor(protocol, p_len, model, m_len, id, datatypes): 223 | sensors = [{'protocol': b"proto_1", 'model': b"model_1", 'id': 1, 224 | 'datatypes': TELLSTICK_TEMPERATURE}, 225 | {'protocol': b"proto_2", 'model': b"model_2", 'id': 2, 226 | 'datatypes': TELLSTICK_HUMIDITY}, 227 | {'protocol': b"proto_3", 'model': b"model_3", 'id': 3, 228 | 'datatypes': TELLSTICK_RAINRATE}, 229 | {'protocol': b"proto_4", 'model': b"model_4", 'id': 4, 230 | 'datatypes': TELLSTICK_RAINTOTAL}, 231 | {'protocol': b"proto_5", 'model': b"model_5", 'id': 5, 232 | 'datatypes': TELLSTICK_WINDDIRECTION}, 233 | {'protocol': b"proto_6", 'model': b"model_6", 'id': 6, 234 | 'datatypes': TELLSTICK_WINDAVERAGE}, 235 | {'protocol': b"proto_7", 'model': b"model_7", 'id': 7, 236 | 'datatypes': TELLSTICK_WINDGUST}] 237 | if self.sensor_index < len(sensors): 238 | sensor = sensors[self.sensor_index] 239 | self.sensor_index += 1 240 | 241 | protocol.value = sensor['protocol'] 242 | model.value = sensor['model'] 243 | try: 244 | id._obj.value = sensor['id'] 245 | datatypes._obj.value = sensor['datatypes'] 246 | except AttributeError: 247 | # With pypy we must use contents instead of _obj 248 | id.contents.value = sensor['id'] 249 | datatypes.contents.value = sensor['datatypes'] 250 | return TELLSTICK_SUCCESS 251 | else: 252 | self.sensor_index = 0 253 | return TELLSTICK_ERROR_DEVICE_NOT_FOUND 254 | self.mocklib.tdSensor = tdSensor 255 | 256 | def tdSensorValue(protocol, model, id, datatype, value, v_len, times): 257 | if datatype == 1 << (id - 1): 258 | value.value = self.encode_string("%d" % (id * 100 + datatype)) 259 | return TELLSTICK_SUCCESS 260 | else: 261 | return TELLSTICK_ERROR_METHOD_NOT_SUPPORTED 262 | self.mocklib.tdSensorValue = tdSensorValue 263 | 264 | core = TelldusCore() 265 | sensors = core.sensors() 266 | 267 | self.assertEqual(7, len(sensors)) 268 | self.assertEqual(["proto_%d" % i for i in range(1, 8)], 269 | [s.protocol for s in sensors]) 270 | self.assertEqual(["model_%d" % i for i in range(1, 8)], 271 | [s.model for s in sensors]) 272 | self.assertEqual(list(range(1, 8)), 273 | [s.id for s in sensors]) 274 | self.assertEqual([TELLSTICK_TEMPERATURE, TELLSTICK_HUMIDITY, 275 | TELLSTICK_RAINRATE, TELLSTICK_RAINTOTAL, 276 | TELLSTICK_WINDDIRECTION, TELLSTICK_WINDAVERAGE, 277 | TELLSTICK_WINDGUST], 278 | [s.datatypes for s in sensors]) 279 | 280 | self.assertEqual([False] * 0 + [True] + [False] * 6, 281 | [s.has_temperature() for s in sensors]) 282 | self.assertEqual([False] * 1 + [True] + [False] * 5, 283 | [s.has_humidity() for s in sensors]) 284 | self.assertEqual([False] * 2 + [True] + [False] * 4, 285 | [s.has_rainrate() for s in sensors]) 286 | self.assertEqual([False] * 3 + [True] + [False] * 3, 287 | [s.has_raintotal() for s in sensors]) 288 | self.assertEqual([False] * 4 + [True] + [False] * 2, 289 | [s.has_winddirection() for s in sensors]) 290 | self.assertEqual([False] * 5 + [True] + [False] * 1, 291 | [s.has_windaverage() for s in sensors]) 292 | self.assertEqual([False] * 6 + [True] + [False] * 0, 293 | [s.has_windgust() for s in sensors]) 294 | 295 | self.assertEqual("%d" % (100 + TELLSTICK_TEMPERATURE), 296 | sensors[0].temperature().value) 297 | self.assertEqual("%d" % (200 + TELLSTICK_HUMIDITY), 298 | sensors[1].humidity().value) 299 | self.assertEqual("%d" % (300 + TELLSTICK_RAINRATE), 300 | sensors[2].rainrate().value) 301 | self.assertEqual("%d" % (400 + TELLSTICK_RAINTOTAL), 302 | sensors[3].raintotal().value) 303 | self.assertEqual("%d" % (500 + TELLSTICK_WINDDIRECTION), 304 | sensors[4].winddirection().value) 305 | self.assertEqual("%d" % (600 + TELLSTICK_WINDAVERAGE), 306 | sensors[5].windaverage().value) 307 | self.assertEqual("%d" % (700 + TELLSTICK_WINDGUST), 308 | sensors[6].windgust().value) 309 | 310 | if __name__ == '__main__': 311 | unittest.main() 312 | --------------------------------------------------------------------------------