Repository: quentinsf/qhue
Branch: master
Commit: a8d65c68ed2c
Files: 13
Total size: 83.2 KB
Directory structure:
gitextract__rkadt83/
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── Qhue playground.ipynb
├── README-remote.md
├── README.md
├── examples/
│ └── qhue_example.py
├── qhue/
│ ├── __init__.py
│ ├── oauth_receiver.py
│ ├── qhue.py
│ └── qhue_remote.py
├── requirements.txt
└── setup.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
.ipynb_checkpoints
================================================
FILE: CHANGELOG.md
================================================
# Qhue change log
## 2.0.1 - 2021-10-31
* Fix issue with create_new_username
## 2.0
* Now requires Python 3.
* Offers remote access to bridge if not on the LAN. See [README-remote](README-remote.md) for more info.
* The QhueException, if thrown, contains the type and address info.
## 1.0.12 - 2019-01-04
* No functional changes, just packaging tweaks to include the README as the package description.
## 1.0.11 - 2019-01-04
* 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.
## 1.0.10 - 2018-11-15
* 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.)
## 1.0.9 - 2017-12-10
* Demonstration notebook is more Python3-compatible.
* qhue_example.py will run under Python 2 or 3.
* create_new_username now takes note of devicetype argument if specified.
## 1.0.8 - 2017-06-05
* Creation of the short_address attribute would fail when a username was not yet assigned. Fixed.
## 1.0.7 - 2017-05-14
* README tweaks
* Example Jupyter notebook has python3-type print statements and tidier example output.
## 1.0.6 - 2017-05-14
* README updates
* Addition of short_address attribute
## 1.0.5 - 2016-11-16
* Updates to documentation
* Inclusion of an example iPython Notebook
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
================================================
FILE: Qhue playground.ipynb
================================================
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Qhue experiments\n",
"\n",
"Experiments with the [Qhue](https://github.com/quentinsf/qhue) python module.\n",
"\n",
"If you haven't already, then `pip install qhue` before starting. \n",
"\n",
"Some of these examples may assume you have a recent bridge with recent software.\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!*"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Basics "
]
},
{
"cell_type": "code",
"execution_count": 49,
"metadata": {},
"outputs": [],
"source": [
"# Put in the IP address of your Hue bridge here\n",
"BRIDGE_IP='192.168.0.45'\n",
"\n",
"from qhue import Bridge, QhueException, create_new_username\n"
]
},
{
"cell_type": "code",
"execution_count": 54,
"metadata": {},
"outputs": [],
"source": [
"# If you have a username set up on your bridge, enter it here\n",
"# otherwise leave it as None and you'll be prompted to create one.\n",
"# e.g.:\n",
"# username='zeZomfNu-y-p1PLM9oeYTiXbtqsxn-q1-7RNLI4B'\n",
"username=None\n",
"\n",
"if username is None:\n",
" username = create_new_username(BRIDGE_IP)\n",
" print(\"New user: {} . Put this in the username variable above.\".format(username))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's get the numbers and names of the lights:"
]
},
{
"cell_type": "code",
"execution_count": 55,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Kitchen Sink 3\n",
"Landing 4\n",
"Top Landing 5\n",
"Kitchen Stove 6\n",
"Front hall 1 7\n",
"Front hall 2 8\n",
"...\n"
]
}
],
"source": [
"bridge = Bridge(BRIDGE_IP, username)\n",
"lights = bridge.lights()\n",
"for num, info in lights.items():\n",
" print(\"{:16} {}\".format(info['name'], num))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's try interactively changing a light. You could make this a lot more sophisticated:"
]
},
{
"cell_type": "code",
"execution_count": 56,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "dbe794dd9d1f4509984b3f0c65a78719"
}
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"from ipywidgets import interact, interactive, fixed\n",
"import ipywidgets as widgets\n",
"\n",
"def setlight(lightid='14', on=True, ct=128, bri=128):\n",
" bridge.lights[lightid].state(on=on)\n",
" if on:\n",
" bridge.lights[lightid].state(bri=bri, ct=ct)\n",
"\n",
"light_list = interact(setlight,\n",
" lightid = widgets.Dropdown(\n",
" options={ lights[i]['name']:i for i in lights },\n",
" value='14',\n",
" description='Light:',\n",
" ),\n",
" on = widgets.Checkbox(value=True, description='On/off'),\n",
" bri = widgets.IntSlider(min=0,max=255,value=128, description='Bright:'),\n",
" ct = widgets.IntSlider(min=0,max=255,value=128, description='Colour:'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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",
"If you haven't got the Python yaml module, `pip install PyYAML`."
]
},
{
"cell_type": "code",
"execution_count": 57,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"20 lights:\n",
"\n",
"'11':\n",
" manufacturername: Philips\n",
" modelid: LCT007\n",
" name: Kitchen table\n",
" state:\n",
" alert: none\n",
" bri: 169\n",
" colormode: xy\n",
" ct: 410\n",
" effect: none\n",
" hue: 14164\n",
" 'on': false\n",
" reachable: true\n",
" sat: 178\n",
" xy: [0.4837, 0.4144]\n",
" swupdate: {lastinstall: null, state: noupdates}\n",
" swversion: 5.50.1.19085\n",
" type: Extended color light\n",
" uniqueid: 00:17:88:01:00:f7:e8:58-0b\n",
"'12':\n",
" manufacturername: Philips\n",
" modelid: LCT007\n",
" name: Kitchen centre\n",
" state:\n",
" alert: none\n",
" bri: 240\n",
" colormode: xy\n",
" ct: 382\n",
" effect: none\n",
" hue: 14665\n",
" 'on': false\n",
" reachable: true\n",
" sat: 156\n",
" xy: [0.4677, 0.4121]\n",
" swupdate: {lastinstall: null, state: noupdates}\n",
" swversion: 5.50.1.19085\n",
" type: Extended color light\n",
" uniqueid: 00:17:88:01:00:f6:e4:98-0b\n",
"...\n",
"\n"
]
}
],
"source": [
"import yaml\n",
"print(\"{} lights:\\n\".format(len(lights)))\n",
"print(yaml.safe_dump(lights, indent=4))"
]
},
{
"cell_type": "code",
"execution_count": 58,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"manufacturername: Philips\n",
"modelid: LCT001\n",
"name: Kitchen Sink\n",
"state:\n",
" alert: none\n",
" bri: 240\n",
" colormode: xy\n",
" ct: 343\n",
" effect: none\n",
" hue: 15360\n",
" 'on': false\n",
" reachable: true\n",
" sat: 119\n",
" xy: [0.4436, 0.4062]\n",
"swupdate: {lastinstall: null, state: noupdates}\n",
"swversion: 5.23.1.13452\n",
"type: Extended color light\n",
"uniqueid: 00:17:88:01:00:d3:13:6c-0b\n",
"\n"
]
}
],
"source": [
"print(yaml.safe_dump(bridge.lights['3'](), indent=4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Scenes "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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",
"Version 1-type scenes just refer to the lights - each light is told: \"Set the value you have stored for this scene\".\n",
"\n",
"Version 2 scenes have more details stored in the hub, which is generally more useful."
]
},
{
"cell_type": "code",
"execution_count": 59,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"141 scenes:\n",
"\n",
"19JUE2wqOsdtine:\n",
" appdata: {data: TOScI_r04_d99, version: 1}\n",
" lastupdated: '2017-02-25T20:07:42'\n",
" lights: ['3', '6', '11', '12', '15', '16', '17', '18', '27', '34']\n",
" locked: false\n",
" name: Dining 3\n",
" owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\n",
" picture: ''\n",
" recycle: false\n",
" version: 2\n",
"342dc4014-on-0:\n",
" appdata: {}\n",
" lastupdated: null\n",
" lights: ['4', '5']\n",
" locked: false\n",
" name: Landing low glow\n",
" owner: none\n",
" picture: ''\n",
" recycle: true\n",
" version: 1\n",
"351acdcd6-on-0:\n",
" appdata: {}\n",
" lastupdated: null\n",
" lights: ['7', '8']\n",
" locked: true\n",
" name: Hall low glow on\n",
" owner: none\n",
" picture: ''\n",
" recycle: true\n",
" version: 1\n",
"SUThT3XiV7sSzml:\n",
" appdata: {data: VKza7_r06_d99, version: 1}\n",
" lastupdated: '2017-02-01T07:54:30'\n",
" lights: ['14', '21', '32', '33']\n",
" locked: true\n",
" name: Warm\n",
" owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\n",
" picture: ''\n",
" recycle: false\n",
" version: 2\n",
"...\n",
"\n"
]
}
],
"source": [
"scenes = bridge.scenes()\n",
"print(\"{} scenes:\\n\".format(len(scenes)))\n",
"print(yaml.safe_dump(scenes, indent=4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Details of a particular scene from the list:"
]
},
{
"cell_type": "code",
"execution_count": 60,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"appdata: {data: skbwq_r06_d06, version: 1}\n",
"lastupdated: '2017-02-01T07:54:30'\n",
"lights: ['14', '21', '32', '33']\n",
"lightstates:\n",
" '14': {bri: 77, ct: 366, 'on': true}\n",
" '21': {bri: 77, ct: 366, 'on': true}\n",
" '32': {bri: 77, ct: 367, 'on': true}\n",
" '33': {bri: 77, ct: 367, 'on': true}\n",
"locked: false\n",
"name: Dimmed\n",
"owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\n",
"picture: ''\n",
"recycle: false\n",
"version: 2\n",
"\n"
]
}
],
"source": [
"print(yaml.safe_dump(bridge.scenes['wVXtOrFmdnySqUz']()))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's list scenes with IDs, last updated time, and the lights affected:"
]
},
{
"cell_type": "code",
"execution_count": 61,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"SePln7Lt9-H7Hm7 Bright 2017-02-01T09:52:06\n",
" - Sitting room 2\n",
" - Sitting room 1\n",
" - Mantelpiece R\n",
" - Mantelpiece L\n",
"\n",
"7264de849-on-0 Hall low glow on None\n",
" - Front hall 1\n",
" - Front hall 2\n",
"\n",
"77470a3f5-off-0 2 lights off None\n",
" - Front hall 1\n",
" - Front hall 2\n",
"\n",
" ...\n",
"\n"
]
}
],
"source": [
"for sid, info in scenes.items():\n",
" print(\"\\n{:16} {:20} {}\".format( sid, info['name'], info['lastupdated']))\n",
" for li in info['lights']:\n",
" print(\"{:40}- {}\".format('', lights[li]['name']))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Tidying things up; let's delete a scene:"
]
},
{
"cell_type": "code",
"execution_count": 62,
"metadata": {},
"outputs": [],
"source": [
"# Uncomment and edit this if you actually want to run it!\n",
"# print(bridge.scenes['cd06c70f7-on-0'](http_method='delete'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Show the details of the scenes that affect a particular light:"
]
},
{
"cell_type": "code",
"execution_count": 63,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Light 21 - Sitting room 1\n",
"SePln7Lt9-H7Hm7 : Bright 2017-02-01T09:52:06\n",
"VaknVPkUZnSrdiB : Bright 2017-02-01T07:54:30\n",
"3owQUn01W7nVsxR : Evening 2017-02-01T07:54:29\n",
"GYOWpf6lHjaVc3T : Off 2016-09-13T21:11:19\n",
"wG25IXpcHHTim4g : Off 2017-02-01T07:54:30\n",
"SUThT3XiV7sSzml : Warm 2017-02-01T07:54:30\n",
"KZNM2DZmdcydRIc : All warm 2016-08-06T23:50:55\n",
"IB57U3scrj4cQWk : Read 2017-02-01T07:54:29\n",
"wVXtOrFmdnySqUz : Dimmed 2017-02-01T07:54:30\n",
"YDfVlYFWoaL6yv5 : Nightlight 2017-02-01T07:54:29\n"
]
}
],
"source": [
"lightname = 'Sitting room 1'\n",
"# How's this for a nice use of python iterators?\n",
"light_id = next(i for i,info in lights.items() if info['name'] == lightname)\n",
"print(\"Light {} - {}\".format(light_id, lightname))\n",
"for line in [\"{} : {:20} {}\".format(sid, info['name'], info['lastupdated']) for sid, info in scenes.items() if light_id in info['lights']]:\n",
" print(line)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Groups and rooms"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's look at groups:"
]
},
{
"cell_type": "code",
"execution_count": 64,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"'1':\n",
" action:\n",
" alert: none\n",
" bri: 240\n",
" colormode: xy\n",
" ct: 343\n",
" effect: none\n",
" hue: 15360\n",
" 'on': false\n",
" sat: 119\n",
" xy: [0.4436, 0.4062]\n",
" lights: ['3', '6']\n",
" name: Kitchen\n",
" recycle: false\n",
" state: {all_on: false, any_on: false}\n",
" type: LightGroup\n",
"'2':\n",
" action:\n",
" alert: none\n",
" bri: 126\n",
" colormode: xy\n",
" ct: 267\n",
" effect: none\n",
" hue: 16528\n",
" 'on': false\n",
" sat: 29\n",
" xy: [0.3944, 0.385]\n",
" lights: ['7', '8']\n",
" name: Hall\n",
" recycle: false\n",
" state: {all_on: false, any_on: false}\n",
" type: LightGroup\n",
"'3':\n",
" action:\n",
" alert: none\n",
" bri: 240\n",
" colormode: xy\n",
" ct: 343\n",
" effect: none\n",
" hue: 15360\n",
" 'on': false\n",
" sat: 119\n",
" xy: [0.4436, 0.4062]\n",
" lights: ['12', '11', '6', '3']\n",
" name: Dimmer 11\n",
" recycle: false\n",
" state: {all_on: false, any_on: false}\n",
" type: LightGroup\n",
"...\n",
"\n"
]
}
],
"source": [
"print(yaml.safe_dump(bridge.groups(), indent=4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The current Hue software creates 'rooms', which are groups with a type value set to Room:"
]
},
{
"cell_type": "code",
"execution_count": 65,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"4 : Kitchen\n",
"5 : Garden\n",
"6 : Sitting room\n",
"7 : Hall\n",
"8 : Landing\n",
"9 : Bedroom\n"
]
}
],
"source": [
"groups = bridge.groups()\n",
"rooms = [(gid, info['name']) for gid, info in groups.items() if info.get('type') == 'Room' ]\n",
"for room_id, info in rooms:\n",
" print(\"{:3} : {}\".format(room_id, info))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Sensors"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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."
]
},
{
"cell_type": "code",
"execution_count": 66,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Bedroom tap 7 ZGPSwitch\n",
"Daylight 1 Daylight\n",
"Dimmer Switch 11 SceneCycle 14 CLIPGenericStatus\n",
"Dimmer Switch 12 SceneCycle 13 CLIPGenericStatus\n",
"Dining Room 9 ZGPSwitch\n",
"Hall 8 ZGPSwitch\n",
"Hall dimmer 12 ZLLSwitch\n",
"Hall sensor 24 ZLLPresence\n",
"Hue ambient light sensor 1 17 ZLLLightLevel\n",
"Hue ambient light sensor 2 21 ZLLLightLevel\n",
"Hue ambient light sensor 3 25 ZLLLightLevel\n",
"Hue ambient light sensor 4 29 ZLLLightLevel\n",
"Hue temperature sensor 1 15 ZLLTemperature\n",
"Hue temperature sensor 2 19 ZLLTemperature\n",
"Hue temperature sensor 3 23 ZLLTemperature\n",
"Hue temperature sensor 4 27 ZLLTemperature\n",
"Kitchen dimmer 11 ZLLSwitch\n",
"Kitchen tap 2 ZGPSwitch\n",
"Landing sensor 16 ZLLPresence\n",
"Landing tap 3 ZGPSwitch\n",
"Laundry sensor 28 ZLLPresence\n",
"Laundry tap 4 ZGPSwitch\n",
"MotionSensor 16.Companion 35 CLIPGenericStatus\n",
"MotionSensor 20.Companion 22 CLIPGenericStatus\n",
"MotionSensor 24.Companion 26 CLIPGenericStatus\n",
"MotionSensor 28.Companion 36 CLIPGenericStatus\n",
"Sitting room 10 ZGPSwitch\n",
"Top Landing sensor 20 ZLLPresence\n",
"Top Tap 6 ZGPSwitch\n",
"XFDani[4][1]sn:step 32 CLIPGenericStatus\n",
"XFDani[4]sn:state 31 CLIPGenericStatus\n"
]
}
],
"source": [
"sensors = bridge.sensors()\n",
"summary = [(info['name'], i, info['type']) for i,info in sensors.items()]\n",
"# Sort by name\n",
"# Python 2: summary.sort(lambda a,b: cmp(a[0], b[0]))\n",
"# Python 3:\n",
"summary.sort(key = lambda a: a[0])\n",
"for n,i,t in summary:\n",
" print(\"{:30} {:>3} {}\".format(n,i,t))\n",
" #print(bridge.sensors[i]())\n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here's a more complete list:"
]
},
{
"cell_type": "code",
"execution_count": 67,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"'1':\n",
" config: {configured: false, 'on': true, sunriseoffset: 30, sunsetoffset: -30}\n",
" manufacturername: Philips\n",
" modelid: PHDL00\n",
" name: Daylight\n",
" state: {daylight: null, lastupdated: none}\n",
" swversion: '1.0'\n",
" type: Daylight\n",
"'10':\n",
" config: {'on': true}\n",
" manufacturername: Philips\n",
" modelid: ZGPSWITCH\n",
" name: Sitting room\n",
" state: {buttonevent: 34, lastupdated: '2017-07-04T22:03:46'}\n",
" swupdate: {lastinstall: null, state: notupdatable}\n",
" type: ZGPSwitch\n",
" uniqueid: 00:00:00:00:00:41:1f:34-f2\n",
"'11':\n",
" config:\n",
" battery: 84\n",
" 'on': true\n",
" pending: []\n",
" reachable: true\n",
" manufacturername: Philips\n",
" modelid: RWL021\n",
" name: Kitchen dimmer\n",
" state: {buttonevent: 4002, lastupdated: '2017-07-04T09:42:08'}\n",
" swupdate: {lastinstall: null, state: noupdates}\n",
" swversion: 5.45.1.17846\n",
" type: ZLLSwitch\n",
" uniqueid: 00:17:88:01:10:33:28:66-02-fc00\n",
"...\n",
"\n"
]
}
],
"source": [
"print(yaml.safe_dump(bridge.sensors(), indent=4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Rules\n",
"\n",
"Rules map sensor events etc. to actions.\n"
]
},
{
"cell_type": "code",
"execution_count": 68,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"'1':\n",
" actions:\n",
" - address: /groups/4/action\n",
" body: {scene: HfANai28yTRy07O}\n",
" method: PUT\n",
" conditions:\n",
" - {address: /sensors/2/state/lastupdated, operator: dx}\n",
" - {address: /sensors/2/state/buttonevent, operator: eq, value: '18'}\n",
" created: '2016-09-23T09:10:49'\n",
" lasttriggered: '2017-07-04T20:50:01'\n",
" name: Tap 2.4\n",
" owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\n",
" recycle: false\n",
" status: enabled\n",
" timestriggered: 3\n",
"'10':\n",
" actions:\n",
" - address: /groups/8/action\n",
" body: {scene: zvuMOXo8vmShFZK}\n",
" method: PUT\n",
" conditions:\n",
" - {address: /sensors/4/state/lastupdated, operator: dx}\n",
" - {address: /sensors/4/state/buttonevent, operator: eq, value: '18'}\n",
" created: '2016-07-23T11:48:50'\n",
" lasttriggered: none\n",
" name: Tap 4.4\n",
" owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\n",
" recycle: false\n",
" status: enabled\n",
" timestriggered: 0\n",
"...\n",
"\n"
]
}
],
"source": [
"rules = bridge.rules()\n",
"print(yaml.safe_dump(rules, indent=4))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Show the rules triggered by the Sitting Room switch.\n",
"\n",
"For Tap switches, buttons 1,2,3,4 are represented by the values 34,16,17,18 respectively."
]
},
{
"cell_type": "code",
"execution_count": 69,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Switch 10 -- Sitting room\n",
"\n",
"29 Tap 10.3 \n",
" ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\n",
" ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '17'}\n",
" - action address /groups/6/action body {'scene': 'SUThT3XiV7sSzml'} Warm \n",
"30 Tap 10.2 \n",
" ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\n",
" ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '16'}\n",
" - action address /groups/6/action body {'scene': 'SePln7Lt9-H7Hm7'} Bright \n",
"31 Tap 10.4 \n",
" ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\n",
" ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '18'}\n",
" - action address /groups/6/action body {'scene': '3owQUn01W7nVsxR'} Evening \n",
"32 2:huelabs/tap-toggle\n",
" ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '34'}\n",
" ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\n",
" ? condition {'address': '/groups/6/state/any_on', 'operator': 'eq', 'value': 'false'}\n",
" - action address /groups/6/action body {'on': True} \n",
"98 2:huelabs/tap-toggle\n",
" ? condition {'address': '/sensors/10/state/buttonevent', 'operator': 'eq', 'value': '34'}\n",
" ? condition {'address': '/sensors/10/state/lastupdated', 'operator': 'dx'}\n",
" ? condition {'address': '/groups/6/state/any_on', 'operator': 'eq', 'value': 'true'}\n",
" - action address /groups/6/action body {'on': False} \n"
]
}
],
"source": [
"switch = '10' # sitting room\n",
"print(\"Switch {} -- {}\\n\".format(switch, sensors[switch]['name']))\n",
"\n",
"# State changes on the switch will look like this:\n",
"state_string = \"/sensors/{}/state/\".format(switch)\n",
"\n",
"# Look through the rules for once which contain this \n",
"# string in their conditions:\n",
"for rid, info in rules.items():\n",
" this_switch = False\n",
" matching_conditions = [c for c in info['conditions'] if state_string in c['address']]\n",
" if len(matching_conditions) > 0:\n",
" print(\"{:3} {:20}\".format(rid, info['name']))\n",
" for c in info['conditions']:\n",
" print(\" ? condition {}\".format(c))\n",
" for a in info['actions']:\n",
"\n",
" # If the action involves applying a scene, get its name\n",
" scene_name = \"\"\n",
" if 'scene' in a['body']:\n",
" scene_name = scenes[a['body']['scene']]['name']\n",
" \n",
" print(\" - action address {} body {!s:29s} {} \".format( a['address'], a['body'], scene_name))\n",
" "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's see what is actually done by one of these scenes:"
]
},
{
"cell_type": "code",
"execution_count": 70,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"appdata: {data: mdVDQ_r06_d99, version: 1}\n",
"lastupdated: '2017-02-01T07:54:29'\n",
"lights: ['14', '21', '32', '33']\n",
"lightstates:\n",
" '14':\n",
" bri: 189\n",
" 'on': true\n",
" xy: [0.5102, 0.3642]\n",
" '21': {bri: 189, 'on': true}\n",
" '32': {bri: 189, 'on': true}\n",
" '33': {bri: 189, 'on': true}\n",
"locked: true\n",
"name: Evening\n",
"owner: IneFZ4CIEdSQQN4oCGExhsi0cWquxMrZY6tEElKM\n",
"picture: ''\n",
"recycle: false\n",
"version: 2\n",
"\n"
]
}
],
"source": [
"scene='3owQUn01W7nVsxR' # 'Evening' scene button 10.4\n",
"\n",
"s = bridge.scenes[scene]()\n",
"print(yaml.safe_dump(s, indent=4))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.1"
},
"widgets": {
"state": {
"f74b2b4846a447e5af1d44678ee8b297": {
"views": [
{
"cell_index": 7
}
]
}
},
"version": "1.2.0"
}
},
"nbformat": 4,
"nbformat_minor": 1
}
================================================
FILE: README-remote.md
================================================
# Using Qhue for Remote Access
Please make sure you're familiar with the main [README](README.md) before continuing!
Qhue 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?
Remote 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/).
Qhue 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.
## What's needed for remote access?
* 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.
* 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.
* 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.
* 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.
## What does it look like?
If you were using Qhue on your *local* network, you might make the initial connection to your bridge as follows:
```python
from qhue import Bridge
b = Bridge("192.168.0.45", username)
```
To make a remote connection, you would do the following instead:
```python
from qhue import RemoteBridge
b = RemoteBridge(username)
token = b.authorize(app_client_id, app_client_secret)
```
Once the authorisation process has completed, you can use the Bridge reference `b` in just the same way as if it were local.
If you save the token you got back from the `authorize()` method, you can use it next time to avoid authorising manually again:
```python
token = b.authorize(app_client_id, app_client_secret, token=token)
```
## How does the authorisation work?
The 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.
The 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.
But 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?
Well, 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.
The 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.
### Option 1: Manually copying the URL
You 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:
`https://www.google.com/?pkce=0&code=mod6jErp&state=NzLj5g6PQggn9cXpvDpZSD9OiahfmB`
Google 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.
### Option 2: Redirecting to a local URL
When 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:
`https://localhost:8584/?pkce=0&code=mod6jErp&state=NzLj5g6PQggn9cXpvDpZSD9OiahfmB`,
it will close down and hand that information on to Qhue. Neat, eh?
Yes... except there's a small complication.
*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.
You'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:
```
openssl req -x509 -nodes -newkey rsa:2048 -subj '/CN=localhost' -keyout key.pem -out cert.pem -days 365
```
## Can you give me a more complete example?
Assuming you've created the key and certificate as described above, you could do something like this:
```python
import json
import os
from qhue import RemoteBridge
# Replace these with your real values:
# Username from the bridge:
USER_ID = "3q5-IVI73-tTBom-Litr3x8E0CP1viIzhxwyP8Sf"
# Client ID and secret from your app registration:
CLIENT_ID = "TSztodTUx5KCi5O4qJKePGIY52uCKKuP"
CLIENT_SECRET = "BBJvGhDmlsDbBHci"
# Where to save the token after you've authenticated
TOKEN_FILENAME = "access_token.json"
b = RemoteBridge(USER_ID)
if os.path.exists(TOKEN_FILENAME):
# Load an existing token if we already have one
with open(TOKEN_FILENAME) as f:
token = json.load(f)
b.authorize(CLIENT_ID, CLIENT_SECRET, token=token)
else:
# Otherwise, open a browser to authenticate and run a server to
# receive the callback credentials.
token = b.authorize(
CLIENT_ID, CLIENT_SECRET,
open_browser=True, use_local_server=True
)
# Save the token for next time:
with open(TOKEN_FILENAME, "wt") as f:
json.dump(token, f, indent=2)
# Now we should be able interact with the bridge as normal:
print(b.lights())
```
## Notes
If 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.
If 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.
================================================
FILE: README.md
================================================
# Qhue
Qhue (pronounced 'Q') is an exceedingly thin Python wrapper for the Philips Hue API.
I 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.
## Understanding Qhue
Philips, 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!
You 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:
http://[myhub]/api//lights/1
You can read information about light 1 by doing an HTTP GET of this URL, and modify it by doing an HTTP PUT.
In 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.
It 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.
## Installing Qhue
That's easy.
pip install qhue
or, more correctly these days:
python3 -m pip install qhue
You 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).
Please 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.
## Examples
Note: 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.
They 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.
OK. Now those preliminaries are out of the way...
First, let's create a Bridge, which will be your top-level Resource.
```python
# Connect to the bridge with a particular username
from qhue import Bridge
b = Bridge("192.168.0.45", username)
```
You can see the URL of any Resource:
```python
# This should give you something familiar from the API docs:
# the base URL for API calls to your Bridge.
print(b.url)
```
By 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:
```python
lights = b.lights # Creates a new Resource with its own URL
print(lights.url) # Should have '/lights' on the end
```
Or, to show it another way, here's what these look like on my system:
```python
>>> b.url
'http://192.168.0.45/api/sQCpOqFjZT2uYlFa2TNKXFbX0RZ6OhBjlYeUo-8F'
>>> b.lights.url
'http://192.168.0.45/api/sQCpOqFjZT2uYlFa2TNKXFbX0RZ6OhBjlYeUo-8F/lights'
```
Now, 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!
```python
# Not actually included with my bridge, but I can still get a URL for it:
>>> b.phaser_bank.url
'http://192.168.0.45/api/sQCpOqFjZT1uYlFa2TNKXFbX0RZ6OhDjlYeUo-8F/phaser_bank'
```
To make an actual API call to the bridge, we simply *call* the Resource as if it were a function:
```python
# Let's actually call the API and print the results
print(b.lights())
```
Qhue 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:
```python
# Get the bridge's configuration info as a dict,
# and print the ethernet MAC address
print(b.config()['mac'])
```
So we've seen that you can call `b.lights()` and `b.config()`. What other calls can you make to the bridge?
Well, 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:
```python
>>> for k in b(): print(k)
lights
groups
config
schedules
scenes
rules
sensors
resourcelinks
```
and you can explore within these lower levels too:
```python
>>> for k in b.sensors(): print (k)
1
2
4
5
8
...
```
OK, let's think about URLs again.
Ideally, 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.
As an alternative, therefore, Qhue will also let you use dictionary key syntax - for example, `b.lights[1]` or `b.lights[n]`.
```python
# Get information about light 1
print(b.lights[1]())
# or, to do the same thing another way:
print(b['lights'][1]())
```
Alternatively, when you *call* a resource, you can give it arguments, which will be added to its URL when making the call:
```python
# This is the same as the last examples:
print(b('lights', 1))
```
So there are several ways to express the same thing, and you can choose the one which fits most elegantly into your code.
Here'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:
```python
>>> [s['name'] for s in b.sensors().values() if s['state'].get('presence')]
["Quentin's study", "Hall", "Kitchen"]
```
Let's explain that one-liner, by way of revision:
`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.
Each 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.
### Making changes
Now, 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:
```python
b.lights[1].state(bri=128, hue=9000)
```
and you can mix URL-constructing positional arguments with value-setting keyword arguments, if you like:
```python
# Positional arguments are added to the URL.
# Keyword arguments change values.
# So these are equivalent to the previous example:
b.lights(1, 'state', bri=128, hue=9000)
b('lights', 1, 'state', bri=128, hue=9000)
```
When you need to specify boolean true/false values, you should use the native Python True and False.
As 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:
```python
bridge.scenes[scene].lightstates[light](on=True, bri=bri, ct=ct)
```
The above examples cover most simple cases.
So to re-emphasise an important point: keyword arguments are very different from positional arguments here:
**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.**
Sometimes, 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:
```python
# Delete rule 1
b('rules', 1, http_method='delete')
```
If 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:
```python
# Set property "class" to "Hallway".
# The trailing underscore will automatically be removed
# in the property name sent to the bridge.
b.groups[19](class_='Hallway')
```
Finally, 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:
```python
>>> b.groups[1].url
'http://192.168.0.45/api/ac594202624a7211ac44615430a461/groups/1'
>>> b.groups[1].address
'/api/ac594202624a7211ac44615430a461/groups/1'
>>> b.groups[1].short_address
'/groups/1'
```
See the API docs for more information about when you need this.
And, at present, that's about it.
## A couple of hints
* 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:
```python
import yaml
print(yaml.safe_dump(bridge.groups(), indent=4))
```
* 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.
* 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).
* 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.
## Creating a user
If you haven't used the API before, you'll need to create a user account on the bridge.
```python
from qhue import create_new_username
username = create_new_username("192.168.0.45")
```
You'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.
Please have a look at the examples directory for a method to store the username for future sessions.
## Usage notes
Please 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.
An 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.)
## Prerequisites
This requires Python 3. It uses Kenneth Reitz's excellent [requests](http://docs.python-requests.org/en/latest/) module, so you'll need to do:
pip install requests
or something similar before using Qhue. If you installed Qhue itself using pip, this shouldn't be necessary.
## Remote access
Starting 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.
For more information see [[README-remote.md]].
## Licence
This 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.
## Contributing
Suggestions, patches, pull requests welcome. There are many ways this could be improved.
If 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.
Many 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!
[Quentin Stafford-Fraser](http://quentinsf.com)
================================================
FILE: examples/qhue_example.py
================================================
#! /usr/bin/env python3
#
# This prints information about the lights on your hub.
# You'll need to set the IP address of your bridge below.
# It will look for a username for the bridge in a file called
# qhue_username.txt, and if it doesn't find one, it will prompt
# you to create one by pressing the button on the bridge.
import json
from os import path
from qhue import Bridge, QhueException, create_new_username
# the IP address of your bridge
BRIDGE_IP = "192.168.0.45"
# the path for the username credentials file
CRED_FILE_PATH = "qhue_username.txt"
def main():
# check for a credential file
if not path.exists(CRED_FILE_PATH):
while True:
try:
username = create_new_username(BRIDGE_IP)
break
except QhueException as err:
print("Error occurred while creating a new username: {}".format(err))
# store the username in a credential file
with open(CRED_FILE_PATH, "w") as cred_file:
cred_file.write(username)
print("Username saved in", CRED_FILE_PATH)
else:
print("Reading username from", CRED_FILE_PATH)
with open(CRED_FILE_PATH, "r") as cred_file:
username = cred_file.read()
# create the bridge resource, passing the captured username
bridge = Bridge(BRIDGE_IP, username)
# create a lights resource
lights = bridge.lights
# query the API and print the results as JSON
print(json.dumps(lights(), indent=2))
if __name__ == "__main__":
main()
================================================
FILE: qhue/__init__.py
================================================
from .qhue import Bridge, QhueException, create_new_username
from .qhue_remote import RemoteBridge
================================================
FILE: qhue/oauth_receiver.py
================================================
# This is a basic solution for getting the access token from an OAuth 2 server.
#
# When you want to grant a piece of software access to an API, you often need to
# open a web browser, go to that API, authorise the access, and you are then
# redirected to a new URL with the appropriate access token passed in the
# request.
#
# This works fine for web apps, but command-line ones won't normally be
# exposing a URL to which OAuth can redirect. So we run a simple server that
# can be specified as the redirection address, and then make the token available
# when it comes through.
#
# OAuth 2 does require HTTPS, so we have to use a certificate.
# You can generate one and a key in the current directory with:
#
# openssl req -x509 -nodes -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365
#
# We will look for 'key.pem' and 'cert.pem' when serving. This is a self-signed key
# so you'll probably need to tell your browser that it really is OK to go to this URL.
#
# You should specify 'https://localhost:8584' as the redirection address to the API.
from http.server import HTTPServer, BaseHTTPRequestHandler
import ssl
LOCAL_SERVER_PORT = 8584
class CollectorException(Exception):
pass
class TokenReceivingServer(HTTPServer):
"""
A simple HTTP server that listens on the specified port
and stores the URL of the last request it received.
"""
def __init__(self, port):
self.received_request = None
self.port = port
print("Starting a small HTTP server to receive the callback")
super().__init__(('', port), TokenHandler)
def save_request(self, request: str):
self.received_request = request
def last_request(self) -> str:
return self.received_request
class TokenHandler(BaseHTTPRequestHandler):
"""
A little HTTP request handler which expects a token
and stores it in the server.
"""
def do_GET(self):
self.server.save_request(self.path)
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(
"Thank you. Authentication token received. You can close this window.".encode()
)
class TokenCollector():
def __init__(self):
self.http_server = TokenReceivingServer(LOCAL_SERVER_PORT)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('cert.pem', 'key.pem')
self.http_server.socket = context.wrap_socket(
self.http_server.socket,
server_side=True
)
def get_single_request(self):
# Could make this wait until we have something that looks like a token
print(f"Waiting for the callback on port {LOCAL_SERVER_PORT}...")
while True:
self.http_server.handle_request()
req = self.http_server.last_request()
if req is not None:
break
return f"https://localhost:{LOCAL_SERVER_PORT}{req}"
================================================
FILE: qhue/qhue.py
================================================
# Qhue is (c) Quentin Stafford-Fraser 2021
# but distributed under the GPL v2.
# It expects Python v3.
import re
import json
# for hostname retrieval for registering with the bridge
from socket import getfqdn
import requests
__all__ = ("Bridge", "QhueException", "create_new_username")
# default timeout in seconds
_DEFAULT_TIMEOUT = 5
class Resource(object):
"""
A Resource represents an object or collection of objects in the Hue world,
such as a light or a group of lights.
It encapsulates an API method that can be called to examine or modify
those objects, and makes it easy to construct the URLs needed.
When you create a Resource, you are building a URL.
When you call a Resource, you are making a request to that URL with some
parameters.
"""
def __init__(self, url, session, timeout=_DEFAULT_TIMEOUT, object_pairs_hook=None):
self.url = url
self.session = session
self.address = url[url.find("/api"):]
# Also find the bit after the username, if there is one
self.short_address = None
post_username_match = re.search(r"/api/[^/]*(.*)", url)
if post_username_match is not None:
self.short_address = post_username_match.group(1)
self.timeout = timeout
self.object_pairs_hook = object_pairs_hook
def __call__(self, *args, **kwargs):
# Preprocess args and kwargs
url = self.url
for a in args:
url += "/" + str(a)
http_method = kwargs.pop("http_method", "get" if not kwargs else "put").lower()
# From each keyword, strip one trailing underscore if it exists,
# then send them as parameters to the bridge. This allows for
# "escaping" of keywords that might conflict with Python syntax
# or with the specially-handled keyword "http_method".
kwargs = {(k[:-1] if k.endswith("_") else k): v for k, v in kwargs.items()}
if http_method == "put":
r = self.session.put(url, data=json.dumps(kwargs, default=list), timeout=self.timeout)
elif http_method == "post":
r = self.session.post(url, data=json.dumps(kwargs, default=list), timeout=self.timeout)
elif http_method == "delete":
r = self.session.delete(url, timeout=self.timeout)
else:
r = self.session.get(url, timeout=self.timeout)
if r.status_code != 200:
raise QhueException("Received response {c} from {u}".format(c=r.status_code, u=url))
resp = r.json(object_pairs_hook=self.object_pairs_hook)
if type(resp) == list:
# In theory, you can get more than one error from a single call
# so they are returned as a list.
errors = [m["error"] for m in resp if "error" in m]
if errors:
# In general, though, there will only be one error per call
# so we return the type and address of the first one in the
# exception, to keep the exception type simple.
raise QhueException(
message=",".join(e["description"] for e in errors),
type_id=",".join(str(e["type"]) for e in errors),
address=errors[0]['address']
)
return resp
def __getattr__(self, name):
return Resource(
self.url + "/" + str(name),
self.session,
timeout=self.timeout,
object_pairs_hook=self.object_pairs_hook,
)
__getitem__ = __getattr__
def __iter__(self):
raise TypeError(f"'{type(self)}' object is not iterable")
def _local_api_url(ip, username=None):
if username is None:
return "http://{}/api".format(ip)
return "http://{}/api/{}".format(ip, username)
def create_new_username(ip, devicetype=None, timeout=_DEFAULT_TIMEOUT):
"""Interactive helper function to generate a new anonymous username.
Args:
ip: ip address of the bridge
devicetype (optional): devicetype to register with the bridge. If
unprovided, generates a device type based on the local hostname.
timeout (optional, default=5): request timeout in seconds
Raises:
QhueException if something went wrong with username generation (for
example, if the bridge button wasn't pressed).
"""
res = Resource(_local_api_url(ip), requests.Session(), timeout)
prompt = "Press the Bridge button, then press Return: "
input(prompt)
if devicetype is None:
devicetype = "qhue#{}".format(getfqdn())
# raises QhueException if something went wrong
response = res(devicetype=devicetype, http_method="post")
return response[0]["success"]["username"]
class Bridge(Resource):
"""
A Bridge is a Resource that represents the top-level connection to a
Philips Hue Bridge (or 'Hub').
It is the basis for building other Resources that represent the things
managed by that Bridge.
"""
def __init__(self, ip, username, timeout=_DEFAULT_TIMEOUT, object_pairs_hook=None):
"""
Create a new connection to a hue bridge.
If a whitelisted username has not been generated yet, use
create_new_username to have the bridge interactively generate
a random username and then pass it to this function.
Args:
ip: ip address of the bridge
username: valid username for the bridge
timeout (optional, default=5): request timeout in seconds
object_pairs_hook (optional): function called by JSON decoder with
the result of any object literal as an ordered list of pairs.
"""
self.ip = ip
self.username = username
url = _local_api_url(ip, username)
self.session = requests.Session()
super().__init__(url, self.session, timeout=timeout, object_pairs_hook=object_pairs_hook)
class QhueException(Exception):
def __init__(self, message, type_id=None, address=None):
self.message = message
self.type_id = type_id
self.address = address
super().__init__(self.message)
def __str__(self):
return f'QhueException: {self.type_id} -> {self.message}'
================================================
FILE: qhue/qhue_remote.py
================================================
# Access to the Hue Hub when using the remote API
# via the Philips servers.
#
# Qhue is (c) Quentin Stafford-Fraser 2021
# but distributed under the GPL v2.
# It expects Python v3.
import webbrowser
import requests
from requests_oauthlib import OAuth2Session
from typing import Optional
from .qhue import Resource, _DEFAULT_TIMEOUT
from .oauth_receiver import TokenCollector
# Remote API root URL for use if outside the LAN
REMOTE_API_BASE = "https://api.meethue.com/bridge"
OAUTH_AUTHORIZE_URL = "https://api.meethue.com/v2/oauth2/authorize"
OAUTH_TOKEN_URL = "https://api.meethue.com/v2/oauth2/token"
OAUTH_REFRESH_URL = OAUTH_TOKEN_URL
def _remote_api_url(username):
# The username is sometimes called a 'whitelist entry'
# in the API docs. You can get one on your local LAN
# using the create_new_username function as described in the
# README.
# TODO: We aren't yet dealing with the situation where you
# don't have the username but are remote.
return "{}/{}".format(REMOTE_API_BASE, username)
class RemoteBridge(Resource):
"""
A RemoteBridge is a Resource that represents the top-level connection to a
Philips Hue Bridge (or 'Hub') from outside that bridge's local network.
It is the basis for building other Resources that represent the things
managed by that Bridge.
It is similar to a bridge, but uses OAuth authentication to the Philips server.
"""
def __init__(self, username: str, timeout: float = _DEFAULT_TIMEOUT, object_pairs_hook=None):
"""
Create a new connection to a remote Hue bridge.
The 'username' is the same as for a local bridge -
sometimes called a whitelist_identifier in the docs.
It is not the user's identifier on the Philips site.
"""
self.username = username
self.session = requests.Session()
url = _remote_api_url(username)
super().__init__(url, self.session, timeout=timeout, object_pairs_hook=object_pairs_hook)
def authorize(
self,
client_id: str,
client_secret: str,
token: Optional[str] = None,
open_browser: bool = True,
use_local_server: bool = False
):
"""
Open a browser to the Hue site to ask you to authorise remote access.
An existing token can be passed if available, otheriwse authorization will be needed:
If open_browser is True, python will try to open your browser to the necessary URL,
otherwise it will just print it out.
Once authorised, this redirects to an HTTPS address, passing the required token.
TODO: We don't currently refresh tokens.
"""
# Use an oauth session in place of a standard requests session.
self.session = OAuth2Session(client_id, token=token)
self.session.headers["Content-Type"] = "application/json"
if token is not None:
return token
authorization_url, state = self.session.authorization_url(OAUTH_AUTHORIZE_URL)
if open_browser:
print("Opening a browser to take you to", authorization_url)
webbrowser.open_new(authorization_url)
else:
print("Open a browser at", authorization_url)
if use_local_server:
c = TokenCollector()
redirect_response = c.get_single_request()
else:
redirect_response = input('Paste the full redirect URL here:')
print("redirect response is ", redirect_response)
return self.session.fetch_token(
OAUTH_TOKEN_URL,
client_secret=client_secret,
authorization_response=redirect_response)
================================================
FILE: requirements.txt
================================================
requests
================================================
FILE: setup.py
================================================
from setuptools import setup
import sys
if sys.version_info[0] == 2:
sys.exit("Sorry, Python 2 is no longer supported. Please use a version of Qhue < 2.0.")
major_version = 2
minor_version = 0
build_version = 1
version = str(major_version) + "." + str(minor_version) + "." + str(build_version)
with open("README.md", "r") as fh:
long_description = fh.read()
setup(
name="qhue",
python_requires='>3.4',
version=version,
description="Qhue: python wrapper for Philips Hue API",
long_description=long_description,
long_description_content_type="text/markdown",
author="Quentin Stafford-Fraser",
url="https://github.com/quentinsf/qhue",
license="GNU GPL 2",
packages=("qhue",),
install_requires=("requests", "requests_oauthlib"),
)