[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.cache\nnosetests.xml\ncoverage.xml\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n.ipynb_checkpoints\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Qhue change log\n\n## 2.0.1 - 2021-10-31\n\n* Fix issue with create_new_username\n\n## 2.0\n\n* Now requires Python 3.\n* Offers remote access to bridge if not on the LAN. See [README-remote](README-remote.md) for more info.\n* The QhueException, if thrown, contains the type and address info.\n\n## 1.0.12 - 2019-01-04\n\n* No functional changes, just packaging tweaks to include the README as the package description.\n\n## 1.0.11 - 2019-01-04\n\n* If keyword argument names end with an underscore, it is removed before sending to the Bridge.  This means you can use, for example, 'class_', and not clash with Python keywords.\n\n## 1.0.10 - 2018-11-15\n\n* Add the object_pairs_hook option to the Bridge constructor.  This controls how JSON structures returned by the API are converted into Python, so you can use OrderedDicts instead of dicts if you want to preserve the (generally logical) ordering used by the bridge. (This does make dumping the structures as YAML more messy, though.)\n\n## 1.0.9 - 2017-12-10\n\n* Demonstration notebook is more Python3-compatible.\n* qhue_example.py will run under Python 2 or 3.\n* create_new_username now takes note of devicetype argument if specified.\n\n## 1.0.8 - 2017-06-05\n\n* Creation of the short_address attribute would fail when a username was not yet assigned.  Fixed.\n\n## 1.0.7 - 2017-05-14\n\n* README tweaks\n* Example Jupyter notebook has python3-type print statements and tidier example output.\n\n## 1.0.6 - 2017-05-14\n\n* README updates\n* Addition of short_address attribute\n\n## 1.0.5 - 2016-11-16\n\n* Updates to documentation\n* Inclusion of an example iPython Notebook\n\n"
  },
  {
    "path": "LICENSE",
    "content": "GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    {description}\n    Copyright (C) {year}  {fullname}\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  {signature of Ty Coon}, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n\n"
  },
  {
    "path": "Qhue playground.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Qhue experiments\\n\",\n    \"\\n\",\n    \"Experiments with the [Qhue](https://github.com/quentinsf/qhue) python module.\\n\",\n    \"\\n\",\n    \"If you haven't already, then `pip install qhue` before starting.  \\n\",\n    \"\\n\",\n    \"Some of these examples may assume you have a recent bridge with recent software.\\n\",\n    \"\\n\",\n    \"*If you're viewing this with my sample output, I've truncated some of it for readability. I have a lot of lights!*\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Basics \"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 49,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# Put in the IP address of your Hue bridge here\\n\",\n    \"BRIDGE_IP='192.168.0.45'\\n\",\n    \"\\n\",\n    \"from qhue import Bridge, QhueException, create_new_username\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 54,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# If you have a username set up on your bridge, enter it here\\n\",\n    \"# otherwise leave it as None and you'll be prompted to create one.\\n\",\n    \"# e.g.:\\n\",\n    \"# username='zeZomfNu-y-p1PLM9oeYTiXbtqsxn-q1-7RNLI4B'\\n\",\n    \"username=None\\n\",\n    \"\\n\",\n    \"if username is None:\\n\",\n    \"    username = create_new_username(BRIDGE_IP)\\n\",\n    \"    print(\\\"New user: {} . Put this in the username variable above.\\\".format(username))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Let's get the numbers and names of the lights:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 55,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Kitchen Sink     3\\n\",\n      \"Landing          4\\n\",\n      \"Top Landing      5\\n\",\n      \"Kitchen Stove    6\\n\",\n      \"Front hall 1     7\\n\",\n      \"Front hall 2     8\\n\",\n      \"...\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"bridge = Bridge(BRIDGE_IP, username)\\n\",\n    \"lights = bridge.lights()\\n\",\n    \"for num, info in lights.items():\\n\",\n    \"    print(\\\"{:16} {}\\\".format(info['name'], num))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Let's try interactively changing a light.  You could make this a lot more sophisticated:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 56,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"application/vnd.jupyter.widget-view+json\": {\n       \"model_id\": \"dbe794dd9d1f4509984b3f0c65a78719\"\n      }\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"from ipywidgets import interact, interactive, fixed\\n\",\n    \"import ipywidgets as widgets\\n\",\n    \"\\n\",\n    \"def setlight(lightid='14', on=True, ct=128, bri=128):\\n\",\n    \"    bridge.lights[lightid].state(on=on)\\n\",\n    \"    if on:\\n\",\n    \"        bridge.lights[lightid].state(bri=bri, ct=ct)\\n\",\n    \"\\n\",\n    \"light_list = interact(setlight,\\n\",\n    \"                      lightid = widgets.Dropdown(\\n\",\n    \"                            options={ lights[i]['name']:i for i in lights },\\n\",\n    \"                            value='14',\\n\",\n    \"                            description='Light:',\\n\",\n    \"                        ),\\n\",\n    \"                      on = widgets.Checkbox(value=True, description='On/off'),\\n\",\n    \"                      bri = widgets.IntSlider(min=0,max=255,value=128, description='Bright:'),\\n\",\n    \"                      ct = widgets.IntSlider(min=0,max=255,value=128, description='Colour:'))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"The [YAML format](https://en.wikipedia.org/wiki/YAML) is a nice way to view the sometimes large amount of structured information which comes back from the bridge. \\n\",\n    \"\\n\",\n    \"If you haven't got the Python yaml module, `pip install PyYAML`.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 57,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"20 lights:\\n\",\n      \"\\n\",\n      \"'11':\\n\",\n      \"    manufacturername: Philips\\n\",\n      \"    modelid: LCT007\\n\",\n      \"    name: Kitchen table\\n\",\n      \"    state:\\n\",\n      \"        alert: none\\n\",\n      \"        bri: 169\\n\",\n      \"        colormode: xy\\n\",\n      \"        ct: 410\\n\",\n      \"        effect: none\\n\",\n      \"        hue: 14164\\n\",\n      \"        'on': false\\n\",\n      \"        reachable: true\\n\",\n      \"        sat: 178\\n\",\n      \"        xy: [0.4837, 0.4144]\\n\",\n      \"    swupdate: {lastinstall: null, state: noupdates}\\n\",\n      \"    swversion: 5.50.1.19085\\n\",\n      \"    type: Extended color light\\n\",\n      \"    uniqueid: 00:17:88:01:00:f7:e8:58-0b\\n\",\n      \"'12':\\n\",\n      \"    manufacturername: Philips\\n\",\n      \"    modelid: LCT007\\n\",\n      \"    name: Kitchen centre\\n\",\n      \"    state:\\n\",\n      \"        alert: none\\n\",\n      \"        bri: 240\\n\",\n      \"        colormode: xy\\n\",\n      \"        ct: 382\\n\",\n      \"        effect: none\\n\",\n      \"        hue: 14665\\n\",\n      \"        'on': false\\n\",\n      \"        reachable: true\\n\",\n      \"        sat: 156\\n\",\n      \"        xy: [0.4677, 0.4121]\\n\",\n      \"    swupdate: {lastinstall: null, state: noupdates}\\n\",\n      \"    swversion: 5.50.1.19085\\n\",\n      \"    type: Extended color light\\n\",\n      \"    uniqueid: 00:17:88:01:00:f6:e4:98-0b\\n\",\n      \"...\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"import yaml\\n\",\n    \"print(\\\"{} lights:\\\\n\\\".format(len(lights)))\\n\",\n    \"print(yaml.safe_dump(lights, indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 58,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"manufacturername: Philips\\n\",\n      \"modelid: LCT001\\n\",\n      \"name: Kitchen Sink\\n\",\n      \"state:\\n\",\n      \"    alert: none\\n\",\n      \"    bri: 240\\n\",\n      \"    colormode: xy\\n\",\n      \"    ct: 343\\n\",\n      \"    effect: none\\n\",\n      \"    hue: 15360\\n\",\n      \"    'on': false\\n\",\n      \"    reachable: true\\n\",\n      \"    sat: 119\\n\",\n      \"    xy: [0.4436, 0.4062]\\n\",\n      \"swupdate: {lastinstall: null, state: noupdates}\\n\",\n      \"swversion: 5.23.1.13452\\n\",\n      \"type: Extended color light\\n\",\n      \"uniqueid: 00:17:88:01:00:d3:13:6c-0b\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(yaml.safe_dump(bridge.lights['3'](), indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Scenes \"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Let's look at the scenes defined in the bridge, and their IDs.  Some of these may be created manually, and others by the Hue app or other software.\\n\",\n    \"\\n\",\n    \"Version 1-type scenes just refer to the lights - each light is told: \\\"Set the value you have stored for this scene\\\".\\n\",\n    \"\\n\",\n    \"Version 2 scenes have more details stored in the hub, which is generally more useful.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 59,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"141 scenes:\\n\",\n      \"\\n\",\n      \"19JUE2wqOsdtine:\\n\",\n      \"    appdata: {data: TOScI_r04_d99, version: 1}\\n\",\n      \"    lastupdated: '2017-02-25T20:07:42'\\n\",\n      \"    lights: ['3', '6', '11', '12', '15', '16', '17', '18', '27', '34']\\n\",\n      \"    locked: false\\n\",\n      \"    name: Dining 3\\n\",\n      \"    owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\\n\",\n      \"    picture: ''\\n\",\n      \"    recycle: false\\n\",\n      \"    version: 2\\n\",\n      \"342dc4014-on-0:\\n\",\n      \"    appdata: {}\\n\",\n      \"    lastupdated: null\\n\",\n      \"    lights: ['4', '5']\\n\",\n      \"    locked: false\\n\",\n      \"    name: Landing low glow\\n\",\n      \"    owner: none\\n\",\n      \"    picture: ''\\n\",\n      \"    recycle: true\\n\",\n      \"    version: 1\\n\",\n      \"351acdcd6-on-0:\\n\",\n      \"    appdata: {}\\n\",\n      \"    lastupdated: null\\n\",\n      \"    lights: ['7', '8']\\n\",\n      \"    locked: true\\n\",\n      \"    name: Hall low glow on\\n\",\n      \"    owner: none\\n\",\n      \"    picture: ''\\n\",\n      \"    recycle: true\\n\",\n      \"    version: 1\\n\",\n      \"SUThT3XiV7sSzml:\\n\",\n      \"    appdata: {data: VKza7_r06_d99, version: 1}\\n\",\n      \"    lastupdated: '2017-02-01T07:54:30'\\n\",\n      \"    lights: ['14', '21', '32', '33']\\n\",\n      \"    locked: true\\n\",\n      \"    name: Warm\\n\",\n      \"    owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\\n\",\n      \"    picture: ''\\n\",\n      \"    recycle: false\\n\",\n      \"    version: 2\\n\",\n      \"...\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"scenes = bridge.scenes()\\n\",\n    \"print(\\\"{} scenes:\\\\n\\\".format(len(scenes)))\\n\",\n    \"print(yaml.safe_dump(scenes, indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Details of a particular scene from the list:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 60,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"appdata: {data: skbwq_r06_d06, version: 1}\\n\",\n      \"lastupdated: '2017-02-01T07:54:30'\\n\",\n      \"lights: ['14', '21', '32', '33']\\n\",\n      \"lightstates:\\n\",\n      \"  '14': {bri: 77, ct: 366, 'on': true}\\n\",\n      \"  '21': {bri: 77, ct: 366, 'on': true}\\n\",\n      \"  '32': {bri: 77, ct: 367, 'on': true}\\n\",\n      \"  '33': {bri: 77, ct: 367, 'on': true}\\n\",\n      \"locked: false\\n\",\n      \"name: Dimmed\\n\",\n      \"owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\\n\",\n      \"picture: ''\\n\",\n      \"recycle: false\\n\",\n      \"version: 2\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(yaml.safe_dump(bridge.scenes['wVXtOrFmdnySqUz']()))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Let's list scenes with IDs, last updated time, and the lights affected:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 61,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"\\n\",\n      \"SePln7Lt9-H7Hm7  Bright               2017-02-01T09:52:06\\n\",\n      \"                                        - Sitting room 2\\n\",\n      \"                                        - Sitting room 1\\n\",\n      \"                                        - Mantelpiece R\\n\",\n      \"                                        - Mantelpiece L\\n\",\n      \"\\n\",\n      \"7264de849-on-0   Hall low glow on     None\\n\",\n      \"                                        - Front hall 1\\n\",\n      \"                                        - Front hall 2\\n\",\n      \"\\n\",\n      \"77470a3f5-off-0  2 lights off         None\\n\",\n      \"                                        - Front hall 1\\n\",\n      \"                                        - Front hall 2\\n\",\n      \"\\n\",\n      \" ...\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"for sid, info in scenes.items():\\n\",\n    \"    print(\\\"\\\\n{:16} {:20} {}\\\".format( sid, info['name'], info['lastupdated']))\\n\",\n    \"    for li in info['lights']:\\n\",\n    \"        print(\\\"{:40}- {}\\\".format('', lights[li]['name']))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Tidying things up; let's delete a scene:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 62,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# Uncomment and edit this if you actually want to run it!\\n\",\n    \"# print(bridge.scenes['cd06c70f7-on-0'](http_method='delete'))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Show the details of the scenes that affect a particular light:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 63,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Light 21 - Sitting room 1\\n\",\n      \"SePln7Lt9-H7Hm7 : Bright               2017-02-01T09:52:06\\n\",\n      \"VaknVPkUZnSrdiB : Bright               2017-02-01T07:54:30\\n\",\n      \"3owQUn01W7nVsxR : Evening              2017-02-01T07:54:29\\n\",\n      \"GYOWpf6lHjaVc3T : Off                  2016-09-13T21:11:19\\n\",\n      \"wG25IXpcHHTim4g : Off                  2017-02-01T07:54:30\\n\",\n      \"SUThT3XiV7sSzml : Warm                 2017-02-01T07:54:30\\n\",\n      \"KZNM2DZmdcydRIc : All warm             2016-08-06T23:50:55\\n\",\n      \"IB57U3scrj4cQWk : Read                 2017-02-01T07:54:29\\n\",\n      \"wVXtOrFmdnySqUz : Dimmed               2017-02-01T07:54:30\\n\",\n      \"YDfVlYFWoaL6yv5 : Nightlight           2017-02-01T07:54:29\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"lightname = 'Sitting room 1'\\n\",\n    \"# How's this for a nice use of python iterators?\\n\",\n    \"light_id = next(i for i,info in lights.items() if info['name'] == lightname)\\n\",\n    \"print(\\\"Light {} - {}\\\".format(light_id, lightname))\\n\",\n    \"for line in [\\\"{} : {:20} {}\\\".format(sid, info['name'], info['lastupdated']) for sid, info in scenes.items() if light_id in info['lights']]:\\n\",\n    \"    print(line)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Groups and rooms\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Let's look at groups:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 64,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"'1':\\n\",\n      \"    action:\\n\",\n      \"        alert: none\\n\",\n      \"        bri: 240\\n\",\n      \"        colormode: xy\\n\",\n      \"        ct: 343\\n\",\n      \"        effect: none\\n\",\n      \"        hue: 15360\\n\",\n      \"        'on': false\\n\",\n      \"        sat: 119\\n\",\n      \"        xy: [0.4436, 0.4062]\\n\",\n      \"    lights: ['3', '6']\\n\",\n      \"    name: Kitchen\\n\",\n      \"    recycle: false\\n\",\n      \"    state: {all_on: false, any_on: false}\\n\",\n      \"    type: LightGroup\\n\",\n      \"'2':\\n\",\n      \"    action:\\n\",\n      \"        alert: none\\n\",\n      \"        bri: 126\\n\",\n      \"        colormode: xy\\n\",\n      \"        ct: 267\\n\",\n      \"        effect: none\\n\",\n      \"        hue: 16528\\n\",\n      \"        'on': false\\n\",\n      \"        sat: 29\\n\",\n      \"        xy: [0.3944, 0.385]\\n\",\n      \"    lights: ['7', '8']\\n\",\n      \"    name: Hall\\n\",\n      \"    recycle: false\\n\",\n      \"    state: {all_on: false, any_on: false}\\n\",\n      \"    type: LightGroup\\n\",\n      \"'3':\\n\",\n      \"    action:\\n\",\n      \"        alert: none\\n\",\n      \"        bri: 240\\n\",\n      \"        colormode: xy\\n\",\n      \"        ct: 343\\n\",\n      \"        effect: none\\n\",\n      \"        hue: 15360\\n\",\n      \"        'on': false\\n\",\n      \"        sat: 119\\n\",\n      \"        xy: [0.4436, 0.4062]\\n\",\n      \"    lights: ['12', '11', '6', '3']\\n\",\n      \"    name: Dimmer 11\\n\",\n      \"    recycle: false\\n\",\n      \"    state: {all_on: false, any_on: false}\\n\",\n      \"    type: LightGroup\\n\",\n      \"...\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(yaml.safe_dump(bridge.groups(), indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"The current Hue software creates 'rooms', which are groups with a type value set to Room:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 65,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"4   : Kitchen\\n\",\n      \"5   : Garden\\n\",\n      \"6   : Sitting room\\n\",\n      \"7   : Hall\\n\",\n      \"8   : Landing\\n\",\n      \"9   : Bedroom\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"groups = bridge.groups()\\n\",\n    \"rooms = [(gid, info['name']) for gid, info in groups.items() if info.get('type') == 'Room' ]\\n\",\n    \"for room_id, info in rooms:\\n\",\n    \"    print(\\\"{:3} : {}\\\".format(room_id, info))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Sensors\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Sensors are mostly switches, but a few other things come under the same category in the bridge.  There's a 'daylight' sensor, implemented in software, for example, and various bits of state can also be stored here so they can be used in rule conditions later.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 66,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Bedroom tap                      7 ZGPSwitch\\n\",\n      \"Daylight                         1 Daylight\\n\",\n      \"Dimmer Switch 11 SceneCycle     14 CLIPGenericStatus\\n\",\n      \"Dimmer Switch 12 SceneCycle     13 CLIPGenericStatus\\n\",\n      \"Dining Room                      9 ZGPSwitch\\n\",\n      \"Hall                             8 ZGPSwitch\\n\",\n      \"Hall dimmer                     12 ZLLSwitch\\n\",\n      \"Hall sensor                     24 ZLLPresence\\n\",\n      \"Hue ambient light sensor 1      17 ZLLLightLevel\\n\",\n      \"Hue ambient light sensor 2      21 ZLLLightLevel\\n\",\n      \"Hue ambient light sensor 3      25 ZLLLightLevel\\n\",\n      \"Hue ambient light sensor 4      29 ZLLLightLevel\\n\",\n      \"Hue temperature sensor 1        15 ZLLTemperature\\n\",\n      \"Hue temperature sensor 2        19 ZLLTemperature\\n\",\n      \"Hue temperature sensor 3        23 ZLLTemperature\\n\",\n      \"Hue temperature sensor 4        27 ZLLTemperature\\n\",\n      \"Kitchen dimmer                  11 ZLLSwitch\\n\",\n      \"Kitchen tap                      2 ZGPSwitch\\n\",\n      \"Landing sensor                  16 ZLLPresence\\n\",\n      \"Landing tap                      3 ZGPSwitch\\n\",\n      \"Laundry sensor                  28 ZLLPresence\\n\",\n      \"Laundry tap                      4 ZGPSwitch\\n\",\n      \"MotionSensor 16.Companion       35 CLIPGenericStatus\\n\",\n      \"MotionSensor 20.Companion       22 CLIPGenericStatus\\n\",\n      \"MotionSensor 24.Companion       26 CLIPGenericStatus\\n\",\n      \"MotionSensor 28.Companion       36 CLIPGenericStatus\\n\",\n      \"Sitting room                    10 ZGPSwitch\\n\",\n      \"Top Landing sensor              20 ZLLPresence\\n\",\n      \"Top Tap                          6 ZGPSwitch\\n\",\n      \"XFDani[4][1]sn:step             32 CLIPGenericStatus\\n\",\n      \"XFDani[4]sn:state               31 CLIPGenericStatus\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"sensors = bridge.sensors()\\n\",\n    \"summary = [(info['name'], i, info['type']) for i,info in sensors.items()]\\n\",\n    \"# Sort by name\\n\",\n    \"# Python 2: summary.sort(lambda a,b: cmp(a[0], b[0]))\\n\",\n    \"# Python 3:\\n\",\n    \"summary.sort(key = lambda a: a[0])\\n\",\n    \"for n,i,t in summary:\\n\",\n    \"    print(\\\"{:30} {:>3} {}\\\".format(n,i,t))\\n\",\n    \"    #print(bridge.sensors[i]())\\n\",\n    \"    \"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Here's a more complete list:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 67,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"'1':\\n\",\n      \"    config: {configured: false, 'on': true, sunriseoffset: 30, sunsetoffset: -30}\\n\",\n      \"    manufacturername: Philips\\n\",\n      \"    modelid: PHDL00\\n\",\n      \"    name: Daylight\\n\",\n      \"    state: {daylight: null, lastupdated: none}\\n\",\n      \"    swversion: '1.0'\\n\",\n      \"    type: Daylight\\n\",\n      \"'10':\\n\",\n      \"    config: {'on': true}\\n\",\n      \"    manufacturername: Philips\\n\",\n      \"    modelid: ZGPSWITCH\\n\",\n      \"    name: Sitting room\\n\",\n      \"    state: {buttonevent: 34, lastupdated: '2017-07-04T22:03:46'}\\n\",\n      \"    swupdate: {lastinstall: null, state: notupdatable}\\n\",\n      \"    type: ZGPSwitch\\n\",\n      \"    uniqueid: 00:00:00:00:00:41:1f:34-f2\\n\",\n      \"'11':\\n\",\n      \"    config:\\n\",\n      \"        battery: 84\\n\",\n      \"        'on': true\\n\",\n      \"        pending: []\\n\",\n      \"        reachable: true\\n\",\n      \"    manufacturername: Philips\\n\",\n      \"    modelid: RWL021\\n\",\n      \"    name: Kitchen dimmer\\n\",\n      \"    state: {buttonevent: 4002, lastupdated: '2017-07-04T09:42:08'}\\n\",\n      \"    swupdate: {lastinstall: null, state: noupdates}\\n\",\n      \"    swversion: 5.45.1.17846\\n\",\n      \"    type: ZLLSwitch\\n\",\n      \"    uniqueid: 00:17:88:01:10:33:28:66-02-fc00\\n\",\n      \"...\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"print(yaml.safe_dump(bridge.sensors(), indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Rules\\n\",\n    \"\\n\",\n    \"Rules map sensor events etc. to actions.\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 68,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"'1':\\n\",\n      \"    actions:\\n\",\n      \"    -   address: /groups/4/action\\n\",\n      \"        body: {scene: HfANai28yTRy07O}\\n\",\n      \"        method: PUT\\n\",\n      \"    conditions:\\n\",\n      \"    - {address: /sensors/2/state/lastupdated, operator: dx}\\n\",\n      \"    - {address: /sensors/2/state/buttonevent, operator: eq, value: '18'}\\n\",\n      \"    created: '2016-09-23T09:10:49'\\n\",\n      \"    lasttriggered: '2017-07-04T20:50:01'\\n\",\n      \"    name: Tap 2.4\\n\",\n      \"    owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\\n\",\n      \"    recycle: false\\n\",\n      \"    status: enabled\\n\",\n      \"    timestriggered: 3\\n\",\n      \"'10':\\n\",\n      \"    actions:\\n\",\n      \"    -   address: /groups/8/action\\n\",\n      \"        body: {scene: zvuMOXo8vmShFZK}\\n\",\n      \"        method: PUT\\n\",\n      \"    conditions:\\n\",\n      \"    - {address: /sensors/4/state/lastupdated, operator: dx}\\n\",\n      \"    - {address: /sensors/4/state/buttonevent, operator: eq, value: '18'}\\n\",\n      \"    created: '2016-07-23T11:48:50'\\n\",\n      \"    lasttriggered: none\\n\",\n      \"    name: Tap 4.4\\n\",\n      \"    owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\\n\",\n      \"    recycle: false\\n\",\n      \"    status: enabled\\n\",\n      \"    timestriggered: 0\\n\",\n      \"...\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"rules = bridge.rules()\\n\",\n    \"print(yaml.safe_dump(rules, indent=4))\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Show the rules triggered by the Sitting Room switch.\\n\",\n    \"\\n\",\n    \"For Tap switches, buttons 1,2,3,4 are represented by the values 34,16,17,18 respectively.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 69,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"Switch 10 -- Sitting room\\n\",\n      \"\\n\",\n      \"29  Tap 10.3            \\n\",\n      \"   ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\\n\",\n      \"   ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '17'}\\n\",\n      \"   - action address /groups/6/action body {'scene': 'SUThT3XiV7sSzml'}  Warm \\n\",\n      \"30  Tap 10.2            \\n\",\n      \"   ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\\n\",\n      \"   ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '16'}\\n\",\n      \"   - action address /groups/6/action body {'scene': 'SePln7Lt9-H7Hm7'}  Bright \\n\",\n      \"31  Tap 10.4            \\n\",\n      \"   ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\\n\",\n      \"   ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '18'}\\n\",\n      \"   - action address /groups/6/action body {'scene': '3owQUn01W7nVsxR'}  Evening \\n\",\n      \"32  2:huelabs/tap-toggle\\n\",\n      \"   ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '34'}\\n\",\n      \"   ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\\n\",\n      \"   ? condition {'address': '/groups/6/state/any_on', 'operator': 'eq', 'value': 'false'}\\n\",\n      \"   - action address /groups/6/action body {'on': True}                   \\n\",\n      \"98  2:huelabs/tap-toggle\\n\",\n      \"   ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '34'}\\n\",\n      \"   ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\\n\",\n      \"   ? condition {'address': '/groups/6/state/any_on', 'operator': 'eq', 'value': 'true'}\\n\",\n      \"   - action address /groups/6/action body {'on': False}                  \\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"switch = '10'  # sitting room\\n\",\n    \"print(\\\"Switch {} -- {}\\\\n\\\".format(switch, sensors[switch]['name']))\\n\",\n    \"\\n\",\n    \"# State changes on the switch will look like this:\\n\",\n    \"state_string = \\\"/sensors/{}/state/\\\".format(switch)\\n\",\n    \"\\n\",\n    \"# Look through the rules for once which contain this \\n\",\n    \"# string in their conditions:\\n\",\n    \"for rid, info in rules.items():\\n\",\n    \"    this_switch = False\\n\",\n    \"    matching_conditions = [c for c in info['conditions'] if state_string in c['address']]\\n\",\n    \"    if len(matching_conditions) > 0:\\n\",\n    \"        print(\\\"{:3} {:20}\\\".format(rid, info['name']))\\n\",\n    \"        for c in info['conditions']:\\n\",\n    \"            print(\\\"   ? condition {}\\\".format(c))\\n\",\n    \"        for a in info['actions']:\\n\",\n    \"\\n\",\n    \"            # If the action involves applying a scene, get its name\\n\",\n    \"            scene_name = \\\"\\\"\\n\",\n    \"            if 'scene' in a['body']:\\n\",\n    \"                scene_name = scenes[a['body']['scene']]['name']\\n\",\n    \"            \\n\",\n    \"            print(\\\"   - action address {} body {!s:29s} {} \\\".format( a['address'], a['body'], scene_name))\\n\",\n    \"            \"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Let's see what is actually done by one of these scenes:\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 70,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"appdata: {data: mdVDQ_r06_d99, version: 1}\\n\",\n      \"lastupdated: '2017-02-01T07:54:29'\\n\",\n      \"lights: ['14', '21', '32', '33']\\n\",\n      \"lightstates:\\n\",\n      \"    '14':\\n\",\n      \"        bri: 189\\n\",\n      \"        'on': true\\n\",\n      \"        xy: [0.5102, 0.3642]\\n\",\n      \"    '21': {bri: 189, 'on': true}\\n\",\n      \"    '32': {bri: 189, 'on': true}\\n\",\n      \"    '33': {bri: 189, 'on': true}\\n\",\n      \"locked: true\\n\",\n      \"name: Evening\\n\",\n      \"owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\\n\",\n      \"picture: ''\\n\",\n      \"recycle: false\\n\",\n      \"version: 2\\n\",\n      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"scene='3owQUn01W7nVsxR' # 'Evening' scene button 10.4\\n\",\n    \"\\n\",\n    \"s = bridge.scenes[scene]()\\n\",\n    \"print(yaml.safe_dump(s, indent=4))\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.6.1\"\n  },\n  \"widgets\": {\n   \"state\": {\n    \"f74b2b4846a447e5af1d44678ee8b297\": {\n     \"views\": [\n      {\n       \"cell_index\": 7\n      }\n     ]\n    }\n   },\n   \"version\": \"1.2.0\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 1\n}\n"
  },
  {
    "path": "README-remote.md",
    "content": "# Using Qhue for Remote Access\n\nPlease make sure you're familiar with the main [README](README.md) before continuing!\n\nQhue is a handy way to interact with a Hue lighting system on your own network.  But suppose you wish to run Qhue-based software from somewhere else?  After all, the Hue app on your phone works when you're away from home, if you've enabled 'Out-of-home control' on your bridge.  Could your Python code do the same thing?\n\nRemote access depends on you being authenticated via the Philips servers, and the good news is that, as ever, Philips have done [a good job in documenting this](https://developers.meethue.com/develop/hue-api/remote-api-quick-start-guide/).\n\nQhue version 2.0 and later includes a wrapper in the `qhue_remote.py` file to make this process easy.  This functionality has  deliberately been put in a separate file, partly to keep the main `qhue.py` nice and clean for those who don't need the remote aspects, but also because this is only a first version. Suggestions for improvements are welcome, or you may want to use qhue_remote just as an example for your own code.\n\n\n## What's needed for remote access?\n\n* You should get a username from the bridge as described in the main [README](README.md).  It's possible to do this remotely, but we haven't implemented that yet.  Save the username somewhere: you'll need it later.  In the remote access documentation this username is also known as a 'whitelist identifier' -- they're basically the same thing -- and it will typically be a 40-character string.\n\n* You'll need the 'Out-of-home control' option turned on for your bridge.  In the Hue app, go to *Settings > Hue Bridges > your bridge* and check that it's enabled.\n\n* You'll need a free [Hue developer's account](https://developers.meethue.com). As well as giving you access to all the relevant documentation,  this will allow you to register your app on the [My Apps page](https://developers.meethue.com/my-apps/).  This will give you a 'ClientId' and a 'ClientSecret' that represent your app.\n\n* Remote access is authenticated via the widely-used OAuth 2 system.  The Hue-specific details are documented [here](https://developers.meethue.com/develop/hue-api/remote-authentication-oauth/).  We use Kenneth Reitz's excellent [requests-oauthlib](https://requests-oauthlib.readthedocs.io) library for Python, which is much nicer than doing it all ourselves.  We'll come back to how the authorisation works in a minute.\n\n## What does it look like?\n\nIf you were using Qhue on your *local* network, you might make the initial connection to your bridge as follows:\n\n```python\n    from qhue import Bridge\n    b = Bridge(\"192.168.0.45\", username)\n```\n\nTo make a remote connection, you would do the following instead:\n\n```python\n    from qhue import RemoteBridge\n    b = RemoteBridge(username)\n    token = b.authorize(app_client_id, app_client_secret)\n```\n\nOnce the authorisation process has completed, you can use the Bridge reference `b` in just the same way as if it were local.\n\nIf you save the token you got back from the `authorize()` method, you can use it next time to avoid authorising manually again:\n\n```python\n    token = b.authorize(app_client_id, app_client_secret, token=token)\n```\n\n## How does the authorisation work?\n\nThe OAuth2 procedure is commonly used between two web services, e.g. to allow a plugin on your blog to have access to your Twitter feed.  In that scenario, the plugin would send you to Twitter,  you would approve the request for access to your tweets, and you would then be redirected back to your blog site via a 'Callback URL' that included the necessary credentials for the plugin to use.\n\nThe same thing happens here: when you go to the Hue Developers' Site to register your app, as mentioned above, you need to specify what this 'Callback URL' will be.   Then when you make the `b.authorize()` call, it will open your browser on the necessary Philips web page, you can authorise your app, and it will redirect you to your Callback URL with the appropriate authorisation information.\n\nBut what if you're using Qhue as part of a command-line application, or something else that can't easily listen on a URL for the credentials that will be sent back?\n\nWell, then you have a couple of options.  (We'll assume here that you're running a Qhue-based utility on the command line.)  They're both slightly awkward, but you shouldn't need to do them often if you save the token you get back.  You could come up with other ways of doing this yourself.\n\nThe key thing to know is that *the URL itself isn't very important*: all that matters is that you capture the information in it and give it to the Qhue RemoteBridge so it can do the last stage of getting the access token it needs.\n\n\n### Option 1: Manually copying the URL\n\nYou can set the Callback URL to pretty much anything that handles HTTPS.  For example, you could set it to `https://google.com`, or your own website. Using the Google example, once you've been authenticated, you would then be redirected to a URL that might look like this:\n\n`https://www.google.com/?pkce=0&code=mod6jErp&state=NzLj5g6PQggn9cXpvDpZSD9OiahfmB`\n\nGoogle won't know what to do with that, but it doesn't matter:  it's the bit beginning `?pkce...` that contains the necessary info.  You just need to copy the entire URL (including https://) from your browser address bar and paste it into the prompt that QHue gives you.\n\n\n### Option 2: Redirecting to a local URL\n\nWhen you call `b.authorize()`, you can add an argument of `use_local_server=True`.  Qhue will then run a small local webserver on your machine, which listens on port 8584, and on the Hue website you can specify the callback URL for your app as `https://localhost:8584`.  This little server (defined in `oauth_receiver.py`) will just listen for one connection.  When your browser comes in with the credentials in a URL like this:\n\n`https://localhost:8584/?pkce=0&code=mod6jErp&state=NzLj5g6PQggn9cXpvDpZSD9OiahfmB`,\n\nit will close down and hand that information on to Qhue.  Neat, eh?\n\nYes... except there's a small complication.\n\n*OAuth 2 requires the Callback URL to be HTTPS, not HTTP.*  That means that this little server needs to have a certificate and private key to be able to serve up an HTTPS connection, and because it's a self-signed certificate, your browser will warn you and you'll need to authorize it to make the connection.\n\nYou'll need to generate the certificate and key.  It will look for them as files 'cert.pem' and 'key.pem' in the local directory, and you can create suitable files using:\n\n```\nopenssl req -x509 -nodes -newkey rsa:2048 -subj '/CN=localhost' -keyout key.pem -out cert.pem -days 365\n```\n\n\n## Can you give me a more complete example?\n\nAssuming you've created the key and certificate as described above, you could do something like this:\n\n```python\n\nimport json\nimport os\n\nfrom qhue import RemoteBridge\n\n# Replace these with your real values:\n# Username from the bridge:\nUSER_ID = \"3q5-IVI73-tTBom-Litr3x8E0CP1viIzhxwyP8Sf\"\n# Client ID and secret from your app registration:\nCLIENT_ID = \"TSztodTUx5KCi5O4qJKePGIY52uCKKuP\"\nCLIENT_SECRET = \"BBJvGhDmlsDbBHci\"\n\n# Where to save the token after you've authenticated\nTOKEN_FILENAME = \"access_token.json\"\n\nb = RemoteBridge(USER_ID)\n\nif os.path.exists(TOKEN_FILENAME):\n    # Load an existing token if we already have one\n    with open(TOKEN_FILENAME) as f:\n        token = json.load(f)\n    b.authorize(CLIENT_ID, CLIENT_SECRET, token=token)\n\nelse:\n    # Otherwise, open a browser to authenticate and run a server to\n    # receive the callback credentials.\n    token = b.authorize(\n        CLIENT_ID, CLIENT_SECRET,\n        open_browser=True, use_local_server=True\n    )\n    # Save the token for next time:\n    with open(TOKEN_FILENAME, \"wt\") as f:\n        json.dump(token, f, indent=2)\n\n# Now we should be able interact with the bridge as normal:\nprint(b.lights())\n\n```\n\n## Notes\n\nIf you don't want Qhue to try opening your browser for you to do the authentication -- something that only makes sense anyway if it's running on your local machine -- you can specify `open_browser=False` when calling the `authorize` method and it will then print out the URL you need to open.\n\nIf you don't want to run a local server for receiving the credentials back, you can specify `use_local_server=False` (or omit it completely) and `authorize` will then prompt you on the command line to paste in the Callback URL with its arguments.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Qhue\n\nQhue (pronounced 'Q') is an exceedingly thin Python wrapper for the Philips Hue API.\n\nI wrote it because some of the other (excellent) frameworks out there weren't quite keeping up with developments in the API.  Because Qhue encodes almost none of the underlying models - it's really just a way of constructing URLs and HTTP calls - it should inherit any new API features automatically.  The aim of Qhue is not to create another Python API for the Hue system, so much as to turn the existing API *into* Python, with minimal interference.\n\n## Understanding Qhue\n\nPhilips, to their credit, created a beautiful RESTful API for the Hue system, documented it and made it available from very early on.   If only more manufacturers follwed their example!\n\nYou can (and should) read [the full Philips documentation here](http://www.developers.meethue.com/philips-hue-api), but a quick summary is that resources such as lights, scenes and so forth each have a URL, which might look like this:\n\n    http://[myhub]/api/<username>/lights/1\n\nYou can read information about light 1 by doing an HTTP GET of this URL, and modify it by doing an HTTP PUT.\n\nIn the `qhue` module we have a Resource class, which represents *something that has a URL*. By *calling* an instance of this class, you'll make an HTTP request to the hub on that URL.\n\nIt also has a Bridge class, which is a handy starting point for building Resources (and is itself a Resource).  If that seems a bit abstract, don't worry - all will be made clear below.\n\n## Installing Qhue\n\nThat's easy.\n\n    pip install qhue\n\nor, more correctly these days:\n\n    python3 -m pip install qhue\n\nYou may want to check [GitHub](https://github.com/quentinsf/qhue) for the latest version of the module, and of this documentation.  The very latest code is likely to be on [the 'develop' branch](https://github.com/quentinsf/qhue/tree/develop).\n\nPlease note that Qhue, from version 2.0 onwards, expects Python 3 or later.  If you still need to support Python 2, you should use an earlier version of Qhue.\n\n## Examples\n\nNote: These examples assume you know the IP address of your bridge.  See [the 'Getting Started' section of the API docs](http://www.developers.meethue.com/documentation/getting-started) if you need help in finding it.  I've assigned mine a static address of 192.168.0.45, so that's what you'll see below.\n\nThey also assume you have experimented with the API before, and so have a user account set up on the bridge, and the username stored somewhere.  This is easy to do, but you will need to read the section below entitled 'Creating a user' before actually trying any of the following.\n\nOK.  Now those preliminaries are out of the way...\n\nFirst, let's create a Bridge, which will be your top-level Resource.\n\n```python\n    # Connect to the bridge with a particular username\n    from qhue import Bridge\n    b = Bridge(\"192.168.0.45\", username)\n```\n\nYou can see the URL of any Resource:\n\n```python\n    # This should give you something familiar from the API docs:\n    # the base URL for API calls to your Bridge.\n    print(b.url)\n```\n\nBy requesting most *other* attributes of a Resource object, you will construct a new Resource with the attribute name added to the URL of the original one:\n\n```python\n    lights = b.lights   # Creates a new Resource with its own URL\n    print(lights.url)    # Should have '/lights' on the end\n```\n\nOr, to show it another way, here's what these look like on my system:\n\n```python\n    >>> b.url\n    'http://192.168.0.45/api/sQCpOqFjZT2uYlFa2TNKXFbX0RZ6OhBjlYeUo-8F'\n    >>> b.lights.url\n    'http://192.168.0.45/api/sQCpOqFjZT2uYlFa2TNKXFbX0RZ6OhBjlYeUo-8F/lights'\n```\n\nNow, these Resources are, at this stage, simply *references* to entities on the bridge: they haven't communicated with it yet.  So far, it's just a way of constructing URLs, and you can construct ones which wouldn't actually do anything for you if you tried to use them!\n\n```python\n     # Not actually included with my bridge, but I can still get a URL for it:\n    >>> b.phaser_bank.url\n    'http://192.168.0.45/api/sQCpOqFjZT1uYlFa2TNKXFbX0RZ6OhDjlYeUo-8F/phaser_bank'\n```\n\nTo make an actual API call to the bridge, we simply *call* the Resource as if it were a function:\n\n```python\n    # Let's actually call the API and print the results\n    print(b.lights())\n```\n\nQhue takes the JSON that is returned by the API and turns it back into Python objects, typically a dictionary, so you can access its parts easily, for example:\n\n```python\n    # Get the bridge's configuration info as a dict,\n    # and print the ethernet MAC address\n    print(b.config()['mac'])\n```\n\nSo we've seen that you can call `b.lights()` and `b.config()`. What other calls can you make to the bridge?\n\nWell, you can actually call the bridge itself, and you get back a great big dictionary with everything in it.  It's a bit slow, so if you know what you want, it's better to focus on that specific call.  But by looking at the keys of that dictionary, you can see what the top-level groups are:\n\n```python\n    >>> for k in b(): print(k)\n    lights\n    groups\n    config\n    schedules\n    scenes\n    rules\n    sensors\n    resourcelinks\n```\n\nand you can explore within these lower levels too:\n\n```python\n    >>> for k in b.sensors(): print (k)\n    1\n    2\n    4\n    5\n    8\n    ...\n```\n\nOK, let's think about URLs again.\n\nIdeally, we'd like to be able to construct all of our URLs the same way we did above, so we would refer to light 1 as `b.lights.1`, for example. But this bumps up against a limitation of Python: you can't use numbers as attribute names.  Nor can you use variables.  So we couldn't get light *n* by requesting `b.lights.n` either.\n\nAs an alternative, therefore, Qhue will also let you use dictionary key syntax - for example, `b.lights[1]` or `b.lights[n]`.\n\n```python\n    # Get information about light 1\n    print(b.lights[1]())\n\n    # or, to do the same thing another way:\n    print(b['lights'][1]())\n```\n\nAlternatively, when you *call* a resource, you can give it arguments, which will be added to its URL when making the call:\n\n```python\n    # This is the same as the last examples:\n    print(b('lights', 1))\n```\n\nSo there are several ways to express the same thing, and you can choose the one which fits most elegantly into your code.\n\nHere's another example, and instead of lights, we'll use sensors (switches, motion sensors etc). This one-liner will tell you where people are moving about:\n\n```python\n    >>> [s['name'] for s in b.sensors().values() if s['state'].get('presence')]\n    [\"Quentin's study\", \"Hall\", \"Kitchen\"]\n```\n\nLet's explain that one-liner, by way of revision:\n\n`b.sensors` is a Resource representing your sensors, so `b.sensors()` will make an API call and get back a dict of information about all your sensors, indexed by their ID.  We don't care about the ID keys here, so we use `b.sensors().values()` to get a list containing just the data about each sensor.\n\nEach item in this list is a dict which will include a 'name' and a 'state', and if the state includes a 'presence' with a true value, then it is a motion sensor which is detecting movement.\n\n\n### Making changes\n\nNow, to make a change to a value, such as the brightness of a bulb, you also call the resource, but you add keyword arguments to specify the properties you want to change.  You can change the brightness and hue of a light by setting properties on its *state*, for example:\n\n```python\n    b.lights[1].state(bri=128, hue=9000)\n```\n\nand you can mix URL-constructing positional arguments with value-setting keyword arguments, if you like:\n\n```python\n    # Positional arguments are added to the URL.\n    # Keyword arguments change values.\n    # So these are equivalent to the previous example:\n\n    b.lights(1, 'state', bri=128, hue=9000)\n    b('lights', 1, 'state', bri=128, hue=9000)\n```\n\nWhen you need to specify boolean true/false values, you should use the native Python True and False.\n\nAs a more complex example, if you want to set the brightness and colour temperature of a light in a given scene, you might use a call like this:\n\n```python\n    bridge.scenes[scene].lightstates[light](on=True, bri=bri, ct=ct)\n```\n\nThe above examples cover most simple cases.\n\nSo to re-emphasise an important point: keyword arguments are very different from positional arguments here:\n\n**If you don't have any keyword arguments, the HTTP request will be a GET, and will ***tell you about*** the current status.  If you do have keyword arguments, it will become a PUT, and it will ***change*** the current status.**\n\nSometimes, though, you need to specify a POST or a DELETE, and you can do so with the special *http_method* argument, which will override the above rule:\n\n```python\n    # Delete rule 1\n    b('rules', 1, http_method='delete')\n```\n\nIf you need to specify a keyword argument that would conflict with a Python keyword, such as `class`, simply append an underscore to it, like this:\n\n\n```python\n    # Set property \"class\" to \"Hallway\".\n    # The trailing underscore will automatically be removed\n    # in the property name sent to the bridge.\n\n    b.groups[19](class_='Hallway')\n```\n\nFinally, for certain operations, like schedules and rules, you'll want to know the 'address' of a resource, which is the absolute URL path - the bit after the IP address, or, more recently, the bit after the username.  You can get these with the `address` and `short_address` attributes:\n\n```python\n    >>> b.groups[1].url\n    'http://192.168.0.45/api/ac594202624a7211ac44615430a461/groups/1'\n    >>> b.groups[1].address\n    '/api/ac594202624a7211ac44615430a461/groups/1'\n    >>> b.groups[1].short_address\n    '/groups/1'\n```\n\nSee the API docs for more information about when you need this.\n\nAnd, at present, that's about it.\n\n\n## A couple of hints\n\n* Some of the requests can return large amounts of information.  A handy way to make it more readable is to format it as YAML.  You may need to `pip install PyYAML`, then try the following:\n\n```python\n    import yaml\n    print(yaml.safe_dump(bridge.groups(), indent=4))\n```\n\n* The Bridge generally returns items in a reasonably logical order. The order is not actually important, but if you wish to preserve it, then you probably *don't* want the JSON structures turned into Python dicts, since these do not generally preserve ordering.  When you construct the Bridge object, you can tell it to use another function to turn JSON dictionaries into Python structures, for example by specifying `object_pairs_hook=collections.OrderedDict`. This will give you OrderedDicts instead of dicts, which is a benefit in almost every way, except that any YAML output you create from it won't look so nice.\n\n* If you're familiar with the Jupyter (iPython) Notebook system, it can be a fun way to explore the API.  See the [Qhue Playground example notebook](Qhue%20playground.ipynb).\n\n* If there is an error, a `QhueException` will be raised.  If the error was returned from the API call, as described in [the documentation](https://developers.meethue.com/develop/hue-api/error-messages/), it will have a type and address field as well as the human-readable message, making it easier, for example, to ignore certain types of error.\n\n## Creating a user\n\nIf you haven't used the API before, you'll need to create a user account on the bridge.\n\n```python\n    from qhue import create_new_username\n    username = create_new_username(\"192.168.0.45\")\n```\n\nYou'll get a prompt saying that the link button on the bridge needs to be pressed.  Go and press it, and you should get a generated username. You can now get a new Bridge object as shown in the examples above, passing this username as the second argument.\n\nPlease have a look at the examples directory for a method to store the username for future sessions.\n\n\n## Usage notes\n\nPlease note that qhue won't do any local checking of any method calls or arguments - it just packages up what you give it and sends it to the bridge.\n\nAn important example of this is that the bridge is expecting integer values for things like colour temperature and brightness. If, say, you do a calculation for your colour which returns a float, you need to convert that to an int before sending or it will be ignored.  (Sending a string returns an error, but sending a float does not.)\n\n\n## Prerequisites\n\nThis requires Python 3.  It uses Kenneth Reitz's excellent [requests](http://docs.python-requests.org/en/latest/) module, so you'll need to do:\n\n    pip install requests\n\nor something similar before using Qhue.  If you installed Qhue itself using pip, this shouldn't be necessary.\n\n\n## Remote access\n\nStarting with version 2, Qhue has a wrapper to support remote access: interacting with your Hue hub via the Philips servers when you are at a remote location, in the same way that a phone app might do when you are away from home.\n\nFor more information see [[README-remote.md]].\n\n## Licence\n\nThis little snippet is distributed under the GPL v2. See the LICENSE file. (They spell it that way on the other side of the pond.) It comes with no warranties, express or implied, but just with the hope that it may be useful to someone.\n\n\n## Contributing\n\nSuggestions, patches, pull requests welcome.  There are many ways this could be improved.\n\nIf you can do so in a general way, without adding too many lines, that would be even better!  Brevity, as Polonius said, is the soul of wit.\n\nMany thanks to John Bond, Sander Johansson, Travis Evans, David Coles, Chris Macklin, Andrea Jemmett, Martin Paulus, Ryan Turner, Matthew Clapp, Marcus Klaas de Vries and Richard Morrison, amongst others, for their contributions!\n\n[Quentin Stafford-Fraser](http://quentinsf.com)\n\n\n"
  },
  {
    "path": "examples/qhue_example.py",
    "content": "#! /usr/bin/env python3\n#\n# This prints information about the lights on your hub.\n# You'll need to set the IP address of your bridge below.\n# It will look for a username for the bridge in a file called\n# qhue_username.txt, and if it doesn't find one, it will prompt\n# you to create one by pressing the button on the bridge.\n\nimport json\nfrom os import path\nfrom qhue import Bridge, QhueException, create_new_username\n\n# the IP address of your bridge\nBRIDGE_IP = \"192.168.0.45\"\n\n# the path for the username credentials file\nCRED_FILE_PATH = \"qhue_username.txt\"\n\n\ndef main():\n\n    # check for a credential file\n    if not path.exists(CRED_FILE_PATH):\n\n        while True:\n            try:\n                username = create_new_username(BRIDGE_IP)\n                break\n            except QhueException as err:\n                print(\"Error occurred while creating a new username: {}\".format(err))\n\n        # store the username in a credential file\n        with open(CRED_FILE_PATH, \"w\") as cred_file:\n            cred_file.write(username)\n            print(\"Username saved in\", CRED_FILE_PATH)\n\n    else:\n        print(\"Reading username from\", CRED_FILE_PATH)\n        with open(CRED_FILE_PATH, \"r\") as cred_file:\n            username = cred_file.read()\n\n    # create the bridge resource, passing the captured username\n    bridge = Bridge(BRIDGE_IP, username)\n\n    # create a lights resource\n    lights = bridge.lights\n\n    # query the API and print the results as JSON\n    print(json.dumps(lights(), indent=2))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "qhue/__init__.py",
    "content": "from .qhue import Bridge, QhueException, create_new_username\nfrom .qhue_remote import RemoteBridge\n"
  },
  {
    "path": "qhue/oauth_receiver.py",
    "content": "# This is a basic solution for getting the access token from an OAuth 2 server.\n#\n# When you want to grant a piece of software access to an API, you often need to\n# open a web browser, go to that API, authorise the access, and you are then\n# redirected to a new URL with the appropriate access token passed in the\n# request.\n#\n# This works fine for web apps, but command-line ones won't normally be\n# exposing a URL to which OAuth can redirect. So we run a simple server that\n# can be specified as the redirection address, and then make the token available\n# when it comes through.\n#\n# OAuth 2 does require HTTPS, so we have to use a certificate.\n# You can generate one and a key in the current directory with:\n#\n#    openssl req -x509 -nodes -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365\n#\n# We will look for 'key.pem' and 'cert.pem' when serving.  This is a self-signed key\n# so you'll probably need to tell your browser that it really is OK to go to this URL.\n#\n# You should specify 'https://localhost:8584' as the redirection address to the API.\n\n\nfrom http.server import HTTPServer, BaseHTTPRequestHandler\nimport ssl\n\nLOCAL_SERVER_PORT = 8584\n\n\nclass CollectorException(Exception):\n    pass\n\n\nclass TokenReceivingServer(HTTPServer):\n    \"\"\"\n    A simple HTTP server that listens on the specified port\n    and stores the URL of the last request it received.\n    \"\"\"\n    def __init__(self, port):\n        self.received_request = None\n        self.port = port\n        print(\"Starting a small HTTP server to receive the callback\")\n        super().__init__(('', port), TokenHandler)\n\n    def save_request(self, request: str):\n        self.received_request = request\n\n    def last_request(self) -> str:\n        return self.received_request\n\n\nclass TokenHandler(BaseHTTPRequestHandler):\n    \"\"\"\n    A little HTTP request handler which expects a token\n    and stores it in the server.\n    \"\"\"\n    def do_GET(self):\n        self.server.save_request(self.path)\n        self.send_response(200)\n        self.send_header('Content-type', 'text/plain')\n        self.end_headers()\n        self.wfile.write(\n            \"Thank you.  Authentication token received. You can close this window.\".encode()\n        )\n\n\nclass TokenCollector():\n\n    def __init__(self):\n        self.http_server = TokenReceivingServer(LOCAL_SERVER_PORT)\n        context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)\n        context.load_cert_chain('cert.pem', 'key.pem')\n        self.http_server.socket = context.wrap_socket(\n            self.http_server.socket,\n            server_side=True\n        )\n\n    def get_single_request(self):\n        # Could make this wait until we have something that looks like a token\n        print(f\"Waiting for the callback on port {LOCAL_SERVER_PORT}...\")\n        while True:\n            self.http_server.handle_request()\n            req = self.http_server.last_request()\n            if req is not None:\n                break\n        return f\"https://localhost:{LOCAL_SERVER_PORT}{req}\"\n\n"
  },
  {
    "path": "qhue/qhue.py",
    "content": "# Qhue is (c) Quentin Stafford-Fraser 2021\n# but distributed under the GPL v2.\n# It expects Python v3.\n\nimport re\nimport json\n\n# for hostname retrieval for registering with the bridge\nfrom socket import getfqdn\n\nimport requests\n\n__all__ = (\"Bridge\", \"QhueException\", \"create_new_username\")\n\n# default timeout in seconds\n_DEFAULT_TIMEOUT = 5\n\n\nclass Resource(object):\n    \"\"\"\n    A Resource represents an object or collection of objects in the Hue world,\n    such as a light or a group of lights.\n    It encapsulates an API method that can be called to examine or modify\n    those objects, and makes it easy to construct the URLs needed.\n    When you create a Resource, you are building a URL.\n    When you call a Resource, you are making a request to that URL with some\n    parameters.\n    \"\"\"\n    def __init__(self, url, session, timeout=_DEFAULT_TIMEOUT, object_pairs_hook=None):\n        self.url = url\n        self.session = session\n        self.address = url[url.find(\"/api\"):]\n        # Also find the bit after the username, if there is one\n        self.short_address = None\n        post_username_match = re.search(r\"/api/[^/]*(.*)\", url)\n        if post_username_match is not None:\n            self.short_address = post_username_match.group(1)\n        self.timeout = timeout\n        self.object_pairs_hook = object_pairs_hook\n\n    def __call__(self, *args, **kwargs):\n        # Preprocess args and kwargs\n        url = self.url\n        for a in args:\n            url += \"/\" + str(a)\n        http_method = kwargs.pop(\"http_method\", \"get\" if not kwargs else \"put\").lower()\n\n        # From each keyword, strip one trailing underscore if it exists,\n        # then send them as parameters to the bridge. This allows for\n        # \"escaping\" of keywords that might conflict with Python syntax\n        # or with the specially-handled keyword \"http_method\".\n        kwargs = {(k[:-1] if k.endswith(\"_\") else k): v for k, v in kwargs.items()}\n        if http_method == \"put\":\n            r = self.session.put(url, data=json.dumps(kwargs, default=list), timeout=self.timeout)\n        elif http_method == \"post\":\n            r = self.session.post(url, data=json.dumps(kwargs, default=list), timeout=self.timeout)\n        elif http_method == \"delete\":\n            r = self.session.delete(url, timeout=self.timeout)\n        else:\n            r = self.session.get(url, timeout=self.timeout)\n        if r.status_code != 200:\n            raise QhueException(\"Received response {c} from {u}\".format(c=r.status_code, u=url))\n        resp = r.json(object_pairs_hook=self.object_pairs_hook)\n        if type(resp) == list:\n            # In theory, you can get more than one error from a single call\n            # so they are returned as a list.\n            errors = [m[\"error\"] for m in resp if \"error\" in m]\n            if errors:\n                # In general, though, there will only be one error per call\n                # so we return the type and address of the first one in the \n                # exception, to keep the exception type simple.\n                raise QhueException(\n                    message=\",\".join(e[\"description\"] for e in errors),\n                    type_id=\",\".join(str(e[\"type\"]) for e in errors),\n                    address=errors[0]['address']\n                )\n        return resp\n\n    def __getattr__(self, name):\n        return Resource(\n            self.url + \"/\" + str(name),\n            self.session,\n            timeout=self.timeout,\n            object_pairs_hook=self.object_pairs_hook,\n        )\n\n    __getitem__ = __getattr__\n\n    def __iter__(self):\n        raise TypeError(f\"'{type(self)}' object is not iterable\")\n\n\ndef _local_api_url(ip, username=None):\n    if username is None:\n        return \"http://{}/api\".format(ip)\n    return \"http://{}/api/{}\".format(ip, username)\n\n\ndef create_new_username(ip, devicetype=None, timeout=_DEFAULT_TIMEOUT):\n    \"\"\"Interactive helper function to generate a new anonymous username.\n\n    Args:\n        ip: ip address of the bridge\n        devicetype (optional): devicetype to register with the bridge. If\n            unprovided, generates a device type based on the local hostname.\n        timeout (optional, default=5): request timeout in seconds\n    Raises:\n        QhueException if something went wrong with username generation (for\n            example, if the bridge button wasn't pressed).\n    \"\"\"\n    res = Resource(_local_api_url(ip), requests.Session(), timeout)\n    prompt = \"Press the Bridge button, then press Return: \"\n    input(prompt)\n\n    if devicetype is None:\n        devicetype = \"qhue#{}\".format(getfqdn())\n\n    # raises QhueException if something went wrong\n    response = res(devicetype=devicetype, http_method=\"post\")\n\n    return response[0][\"success\"][\"username\"]\n\n\nclass Bridge(Resource):\n    \"\"\"\n    A Bridge is a Resource that represents the top-level connection to a\n    Philips Hue Bridge (or 'Hub').\n    It is the basis for building other Resources that represent the things\n    managed by that Bridge.\n    \"\"\"\n    def __init__(self, ip, username, timeout=_DEFAULT_TIMEOUT, object_pairs_hook=None):\n        \"\"\"\n        Create a new connection to a hue bridge.\n\n        If a whitelisted username has not been generated yet, use\n        create_new_username to have the bridge interactively generate\n        a random username and then pass it to this function.\n\n        Args:\n            ip: ip address of the bridge\n            username: valid username for the bridge\n            timeout (optional, default=5): request timeout in seconds\n            object_pairs_hook (optional): function called by JSON decoder with\n                the result of any object literal as an ordered list of pairs.\n        \"\"\"\n        self.ip = ip\n        self.username = username\n        url = _local_api_url(ip, username)\n        self.session = requests.Session()\n        super().__init__(url, self.session, timeout=timeout, object_pairs_hook=object_pairs_hook)\n\n\nclass QhueException(Exception):\n    def __init__(self, message, type_id=None, address=None):\n        self.message = message\n        self.type_id = type_id\n        self.address = address\n\n        super().__init__(self.message)\n\n    def __str__(self):\n        return f'QhueException: {self.type_id} -> {self.message}'\n"
  },
  {
    "path": "qhue/qhue_remote.py",
    "content": "# Access to the Hue Hub when using the remote API\n# via the Philips servers.\n#\n# Qhue is (c) Quentin Stafford-Fraser 2021\n# but distributed under the GPL v2.\n# It expects Python v3.\n\nimport webbrowser\nimport requests\nfrom requests_oauthlib import OAuth2Session\nfrom typing import Optional\n\nfrom .qhue import Resource, _DEFAULT_TIMEOUT\nfrom .oauth_receiver import TokenCollector\n\n# Remote API root URL for use if outside the LAN\nREMOTE_API_BASE = \"https://api.meethue.com/bridge\"\n\nOAUTH_AUTHORIZE_URL = \"https://api.meethue.com/v2/oauth2/authorize\"\nOAUTH_TOKEN_URL = \"https://api.meethue.com/v2/oauth2/token\"\nOAUTH_REFRESH_URL = OAUTH_TOKEN_URL\n\n\ndef _remote_api_url(username):\n    # The username is sometimes called a 'whitelist entry'\n    # in the API docs. You can get one on your local LAN\n    # using the create_new_username function as described in the\n    # README.\n    # TODO: We aren't yet dealing with the situation where you\n    # don't have the username but are remote.\n    return \"{}/{}\".format(REMOTE_API_BASE, username)\n\n\nclass RemoteBridge(Resource):\n    \"\"\"\n    A RemoteBridge is a Resource that represents the top-level connection to a\n    Philips Hue Bridge (or 'Hub') from outside that bridge's local network.\n    It is the basis for building other Resources that represent the things\n    managed by that Bridge.\n    It is similar to a bridge, but uses OAuth authentication to the Philips server.\n    \"\"\"\n    def __init__(self, username: str, timeout: float = _DEFAULT_TIMEOUT, object_pairs_hook=None):\n        \"\"\"\n        Create a new connection to a remote Hue bridge.\n        The 'username' is the same as for a local bridge -\n        sometimes called a whitelist_identifier in the docs.\n        It is not the user's identifier on the Philips site.\n        \"\"\"\n        self.username = username\n        self.session = requests.Session()\n        url = _remote_api_url(username)\n        super().__init__(url, self.session, timeout=timeout, object_pairs_hook=object_pairs_hook)\n\n    def authorize(\n        self,\n        client_id: str,\n        client_secret: str,\n        token: Optional[str] = None,\n        open_browser: bool = True,\n        use_local_server: bool = False\n    ):\n        \"\"\"\n        Open a browser to the Hue site to ask you to authorise remote access.\n        An existing token can be passed if available, otheriwse authorization will be needed:\n        If open_browser is True, python will try to open your browser to the necessary URL,\n        otherwise it will just print it out.\n        Once authorised, this redirects to an HTTPS address, passing the required token.\n\n        TODO: We don't currently refresh tokens.\n        \"\"\"\n        # Use an oauth session in place of a standard requests session.\n        self.session = OAuth2Session(client_id, token=token)\n        self.session.headers[\"Content-Type\"] = \"application/json\"\n        if token is not None:\n            return token\n        authorization_url, state = self.session.authorization_url(OAUTH_AUTHORIZE_URL)\n        if open_browser:\n            print(\"Opening a browser to take you to\", authorization_url)\n            webbrowser.open_new(authorization_url)\n        else:\n            print(\"Open a browser at\", authorization_url)\n\n        if use_local_server:\n            c = TokenCollector()\n            redirect_response = c.get_single_request()\n\n        else:\n            redirect_response = input('Paste the full redirect URL here:')\n\n        print(\"redirect response is \", redirect_response)\n\n        return self.session.fetch_token(\n            OAUTH_TOKEN_URL,\n            client_secret=client_secret,\n            authorization_response=redirect_response)\n"
  },
  {
    "path": "requirements.txt",
    "content": "requests\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\nimport sys\n\nif sys.version_info[0] == 2:\n    sys.exit(\"Sorry, Python 2 is no longer supported. Please use a version of Qhue < 2.0.\")\n\nmajor_version = 2\nminor_version = 0\nbuild_version = 1\n\nversion = str(major_version) + \".\" + str(minor_version) + \".\" + str(build_version)\n\nwith open(\"README.md\", \"r\") as fh:\n    long_description = fh.read()\n\nsetup(\n    name=\"qhue\",\n    python_requires='>3.4',\n    version=version,\n    description=\"Qhue: python wrapper for Philips Hue API\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    author=\"Quentin Stafford-Fraser\",\n    url=\"https://github.com/quentinsf/qhue\",\n    license=\"GNU GPL 2\",\n    packages=(\"qhue\",),\n    install_requires=(\"requests\", \"requests_oauthlib\"),\n)\n"
  }
]