├── .gitignore ├── COPYING ├── Makefile.am ├── README.md ├── autogen.sh ├── bin ├── Makefile.am └── luminance.in ├── configure.ac ├── data ├── Makefile.am ├── com.craigcabrey.luminance.gschema.xml ├── icons │ ├── hicolor │ │ ├── 128x128 │ │ │ └── apps │ │ │ │ └── luminance.png │ │ ├── 16x16 │ │ │ └── apps │ │ │ │ └── luminance.png │ │ ├── 256x256 │ │ │ └── apps │ │ │ │ └── luminance.png │ │ ├── 32x32 │ │ │ └── apps │ │ │ │ └── luminance.png │ │ ├── 48x48 │ │ │ └── apps │ │ │ │ └── luminance.png │ │ └── 64x64 │ │ │ └── apps │ │ │ └── luminance.png │ └── link_button.svg ├── luminance.desktop ├── resources.xml └── ui │ ├── about.ui │ ├── bridge.ui │ ├── entity-detail.ui │ ├── entity-list.ui │ ├── entity-row.ui │ ├── group-detail.ui │ ├── groups.ui │ ├── main.ui │ ├── menu.ui │ ├── new-group.ui │ └── setup.ui ├── luminance ├── Makefile.am ├── __init__.py.in ├── application.py └── views │ ├── Makefile.am │ ├── bridge.py │ ├── entity.py │ ├── group.py │ ├── groups.py │ ├── light.py │ ├── setup.py │ ├── util.py │ └── window.py └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # GTK 92 | gschemas.compiled 93 | *.gresource 94 | *.valid 95 | 96 | # Autotools 97 | Makefile 98 | Makefile.in 99 | /autom4te.cache 100 | /autoscan.log 101 | /autoscan-*.log 102 | /aclocal.m4 103 | /compile 104 | /config.h.in 105 | /configure 106 | /configure.scan 107 | /depcomp 108 | /install-sh 109 | /missing 110 | /stamp-h1 111 | /py-compile 112 | /config.status 113 | 114 | bin/luminance 115 | luminance/__init__.py 116 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = bin data luminance 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![icon](https://raw.githubusercontent.com/craigcabrey/luminance/master/data/icons/hicolor/48x48/apps/luminance.png) Luminance 2 | 3 | Luminance is a Philips Hue client for Linux written in Python and GTK+. 4 | 5 | ![Screenshot of UI](https://raw.githubusercontent.com/craigcabrey/luminance/master/screenshot.png) 6 | 7 | ## Features 8 | 9 | *Note*: The application is not release quality at this time. You will encounter 10 | build issues, bugs, and incomplete or non-working features. That said, please 11 | feel free to create an issue with your problem and I will do my best to triage 12 | it. 13 | 14 | * Individually control lights (brightness, color, on/off, effects) 15 | * Manage and control individual groups (brightness, color, on/off, effects) 16 | * Create new and modify existing groups (stored on the bridge, not on your computer) 17 | * Bridge information and management (rescan for lights, check for updates) 18 | 19 | ## Getting Started 20 | 21 | ### Requirements 22 | 23 | * autoconf 2.69 or later 24 | * Python 3.5 or later (earlier versions may work, but they haven't been tested) 25 | * GTK+ 3.18 or later (earlier versions may work, but they haven't been tested) 26 | * [phue](https://github.com/studioimaginaire/phue) 0.8 or later 27 | * [netdisco](https://github.com/home-assistant/netdisco) 0.7 or later 28 | * [requests](https://github.com/kennethreitz/requests) 2.10 or later 29 | * Hue bridge (tested with the first generation only) 30 | 31 | ### Installing 32 | 33 | 1. Clone this repository. 34 | 1. In the cloned repository, run `./autogen.sh`. 35 | 1. If everything works, run `./configure --prefix=/usr && make && sudo make install`. 36 | 37 | ## License 38 | 39 | Copyright (C) 2016 Craig Cabrey 40 | 41 | This program is free software; you can redistribute it and/or 42 | modify it under the terms of the GNU General Public License 43 | as published by the Free Software Foundation; either version 2 44 | of the License, or (at your option) any later version. 45 | 46 | This program is distributed in the hope that it will be useful, 47 | but WITHOUT ANY WARRANTY; without even the implied warranty of 48 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 49 | GNU General Public License for more details. 50 | 51 | You should have received a copy of the GNU General Public License 52 | along with this program; if not, write to the Free Software 53 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 54 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | chmod +x $0 6 | 7 | AUTORECONF=${AUTORECONF:-autoreconf} 8 | ACLOCAL=${ACLOCAL:-aclocal} 9 | AUTOCONF=${AUTOCONF:-autoconf} 10 | AUTOHEADER=${AUTOHEADER:-autoheader} 11 | AUTOMAKE=${AUTOMAKE:-automake} 12 | 13 | # Check we have all tools installed 14 | check_command() { 15 | command -v "${1}" > /dev/null 2>&1 || { 16 | >&2 echo "autogen.sh: could not find \`$1'. \`$1' is required to run autogen.sh." 17 | exit 1 18 | } 19 | } 20 | 21 | check_command "$AUTORECONF" 22 | check_command "$ACLOCAL" 23 | check_command "$AUTOCONF" 24 | check_command "$AUTOHEADER" 25 | check_command "$AUTOMAKE" 26 | 27 | # Absence of pkg-config or misconfiguration can make some odd error 28 | # messages, we check if it is installed correctly. See: 29 | # https://blogs.oracle.com/mandy/entry/autoconf_weirdness 30 | # 31 | # We cannot just check for pkg-config command, we need to check for 32 | # PKG_* macros. The pkg-config command can be defined in ./configure, 33 | # we cannot tell anything when not present. 34 | check_pkg_config() { 35 | grep -q '^AC_DEFUN.*PKG_CHECK_MODULES' aclocal.m4 || { 36 | cat <&2 37 | autogen.sh: could not find PKG_CHECK_MODULES macro. 38 | 39 | Either pkg-config is not installed on your system or 40 | \`pkg.m4' is missing or not found by aclocal. 41 | 42 | If \`pkg.m4' is installed at an unusual location, re-run 43 | \`autogen.sh' by setting \`ACLOCAL_FLAGS': 44 | 45 | ACLOCAL_FLAGS="-I /share/aclocal" ./autogen.sh 46 | 47 | EOF 48 | exit 1 49 | } 50 | } 51 | 52 | 53 | echo "autogen.sh: reconfigure with autoreconf" 54 | ${AUTORECONF} -vif -I m4 || echo "autogen.sh: autoreconf has failed ($?)!" 55 | 56 | echo "autogen.sh: for the next step, run ./configure" 57 | 58 | exit 0 59 | -------------------------------------------------------------------------------- /bin/Makefile.am: -------------------------------------------------------------------------------- 1 | bin_SCRIPTS = luminance 2 | 3 | EXTRA_DIST = \ 4 | luminance.in 5 | $(NULL) 6 | 7 | CLEANFILES = \ 8 | $(bin_SCRIPTS) 9 | $(NULL) 10 | 11 | luminance: luminance.in Makefile 12 | $(SED) \ 13 | -e s!\@pythondir\@!$(pythondir)! \ 14 | -e s!\@pyexecdir\@!$(pyexecdir)! \ 15 | < $< > $@ 16 | chmod a+x $@ 17 | 18 | all-local: luminance 19 | -------------------------------------------------------------------------------- /bin/luminance.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | 6 | if os.path.exists(os.path.join(os.path.dirname(__file__), '../luminance')): 7 | sys.path.insert( 8 | 0, 9 | os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 10 | ) 11 | else: 12 | pyexecdir = '@pyexecdir@' 13 | pythondir = '@pythondir@' 14 | 15 | sys.path.insert(0, pyexecdir) 16 | 17 | if pyexecdir != pythondir: 18 | sys.path.insert(0, pythondir) 19 | 20 | import gi 21 | 22 | gi.require_version('Gtk', '3.0') 23 | 24 | from gi.repository import Gtk 25 | 26 | from luminance.application import Application 27 | 28 | 29 | def install_excepthook(): 30 | old_hook = sys.excepthook 31 | 32 | def new_hook(etype, evalue, etb): 33 | old_hook(etype, evalue, etb) 34 | 35 | while Gtk.main_level(): 36 | Gtk.main_quit() 37 | sys.exit() 38 | 39 | sys.excepthook = new_hook 40 | 41 | if __name__ == '__main__': 42 | install_excepthook() 43 | 44 | application = Application() 45 | application.run(sys.argv) 46 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_PREREQ([2.69]) 2 | AC_INIT([luminance], 3 | m4_esyscmd(echo -n `git describe --always --tags`), 4 | [https://github.com/craigcabrey/luminance/issues], 5 | [luminance], 6 | [https://craigcabrey.github.io/luminance/]) 7 | 8 | AM_INIT_AUTOMAKE([foreign]) 9 | 10 | AM_PATH_PYTHON([3.5]) 11 | 12 | GLIB_GSETTINGS 13 | 14 | DESKTOP_SCHEMAS_REQUIRED_VERSION=3.18.0 15 | GTK_REQUIRED_VERSION=3.18.0 16 | PYGOBJECT_REQUIRED_VERSION=3.20.0 17 | 18 | PKG_CHECK_MODULES([GSETTINGS_DESKTOP_SCHEMAS], [gsettings-desktop-schemas >= $DESKTOP_SCHEMAS_REQUIRED_VERSION]) 19 | PKG_CHECK_MODULES([GTK], [gtk+-3.0 >= $GTK_REQUIRED_VERSION]) 20 | PKG_CHECK_MODULES([PYGOBJECT3], [pygobject-3.0 >= $PYGOBJECT_REQUIRED_VERSION]) 21 | 22 | GLIB_COMPILE_RESOURCES=`$PKG_CONFIG --variable glib_compile_resources gio-2.0` 23 | AC_SUBST(GLIB_COMPILE_RESOURCES) 24 | 25 | AC_PROG_SED 26 | AC_CONFIG_FILES([ 27 | Makefile 28 | bin/Makefile 29 | data/Makefile 30 | luminance/Makefile 31 | luminance/views/Makefile 32 | ]) 33 | 34 | AC_OUTPUT 35 | -------------------------------------------------------------------------------- /data/Makefile.am: -------------------------------------------------------------------------------- 1 | resource_files = $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(srcdir) --sourcedir=$(builddir) --generate-dependencies $(builddir)/resources.xml) 2 | 3 | resources.gresource: resources.xml $(resource_files) 4 | $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(srcdir) --sourcedir=$(builddir) $< 5 | 6 | desktopdir = $(datadir)/applications 7 | desktop_DATA = luminance.desktop 8 | 9 | resourcedir = $(pkgdatadir) 10 | resource_DATA = resources.gresource 11 | 12 | # hicolor icons 13 | hicolor_icon16dir = $(datadir)/icons/hicolor/16x16/apps 14 | hicolor_icon16_DATA = icons/hicolor/16x16/apps/luminance.png 15 | hicolor_icon32dir = $(datadir)/icons/hicolor/32x32/apps 16 | hicolor_icon32_DATA = icons/hicolor/32x32/apps/luminance.png 17 | hicolor_icon48dir = $(datadir)/icons/hicolor/48x48/apps 18 | hicolor_icon48_DATA = icons/hicolor/48x48/apps/luminance.png 19 | hicolor_icon64dir = $(datadir)/icons/hicolor/64x64/apps 20 | hicolor_icon64_DATA = icons/hicolor/64x64/apps/luminance.png 21 | hicolor_icon128dir = $(datadir)/icons/hicolor/128x128/apps 22 | hicolor_icon128_DATA = icons/hicolor/128x128/apps/luminance.png 23 | hicolor_icon256dir = $(datadir)/icons/hicolor/256x256/apps 24 | hicolor_icon256_DATA = icons/hicolor/256x256/apps/luminance.png 25 | hicolor_icon_files = \ 26 | $(hicolor_icon16_DATA) \ 27 | $(hicolor_icon22_DATA) \ 28 | $(hicolor_icon32_DATA) \ 29 | $(hicolor_icon48_DATA) \ 30 | $(hicolor_icon64_DATA) \ 31 | $(hicolor_icon128_DATA) \ 32 | $(hicolor_icon256_DATA) 33 | 34 | install-data-hook: update-icon-cache 35 | uninstall-hook: update-icon-cache 36 | 37 | gtk_update_hicolor_icon_cache = gtk-update-icon-cache -f -t $(datadir)/icons/hicolor 38 | 39 | update-icon-cache: 40 | @-if test -z "$(DESTDIR)"; then \ 41 | echo "Updating GTK hicolor icon cache."; \ 42 | $(gtk_update_hicolor_icon_cache); \ 43 | else \ 44 | echo "Icon cache not updated."; \ 45 | fi 46 | 47 | uidir = $(pkgdatadir)/ui 48 | ui_DATA = $(filter %.ui,$(resource_files)) 49 | 50 | gsettings_SCHEMAS = com.craigcabrey.luminance.gschema.xml 51 | 52 | gschemas.compiled: $(gsettings_SCHEMAS) Makefile 53 | $(AM_V_GEN) $(GLIB_COMPILE_SCHEMAS) $(builddir) 54 | 55 | @GSETTINGS_RULES@ 56 | 57 | EXTRA_DIST = \ 58 | $(hicolor_icon_files) \ 59 | $(resource_files) \ 60 | luminance.desktop \ 61 | resources.xml \ 62 | com.craigcabrey.luminance.gschema.xml \ 63 | $(NULL) 64 | 65 | CLEANFILES = \ 66 | resources.gresource \ 67 | *.valid \ 68 | gschemas.compiled \ 69 | $(NULL) 70 | 71 | all-local: gschemas.compiled 72 | -------------------------------------------------------------------------------- /data/com.craigcabrey.luminance.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | "" 5 | Philips Hue bridge host address 6 | 7 | Host address of the Philips Hue bridge to use. 8 | 9 | 10 | 11 | "" 12 | Philips Hue bridge username 13 | 14 | Username to use for authentication with the Philips Hue bridge. 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /data/icons/hicolor/128x128/apps/luminance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigcabrey/luminance/33a274c4254c71cb414e0ac93ee8dd02bc8e508e/data/icons/hicolor/128x128/apps/luminance.png -------------------------------------------------------------------------------- /data/icons/hicolor/16x16/apps/luminance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigcabrey/luminance/33a274c4254c71cb414e0ac93ee8dd02bc8e508e/data/icons/hicolor/16x16/apps/luminance.png -------------------------------------------------------------------------------- /data/icons/hicolor/256x256/apps/luminance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigcabrey/luminance/33a274c4254c71cb414e0ac93ee8dd02bc8e508e/data/icons/hicolor/256x256/apps/luminance.png -------------------------------------------------------------------------------- /data/icons/hicolor/32x32/apps/luminance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigcabrey/luminance/33a274c4254c71cb414e0ac93ee8dd02bc8e508e/data/icons/hicolor/32x32/apps/luminance.png -------------------------------------------------------------------------------- /data/icons/hicolor/48x48/apps/luminance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigcabrey/luminance/33a274c4254c71cb414e0ac93ee8dd02bc8e508e/data/icons/hicolor/48x48/apps/luminance.png -------------------------------------------------------------------------------- /data/icons/hicolor/64x64/apps/luminance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigcabrey/luminance/33a274c4254c71cb414e0ac93ee8dd02bc8e508e/data/icons/hicolor/64x64/apps/luminance.png -------------------------------------------------------------------------------- /data/icons/link_button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 12 | 15 | 16 | 33 | 34 | -------------------------------------------------------------------------------- /data/luminance.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Encoding=UTF-8 4 | Name=Luminance 5 | Comment=A Philips Hue client written in Python and GTK+ 6 | Icon=luminance 7 | Exec=luminance 8 | Terminal=false 9 | Categories=GTK;GNOME 10 | -------------------------------------------------------------------------------- /data/resources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | icons/link_button.svg 5 | ui/about.ui 6 | ui/bridge.ui 7 | ui/entity-detail.ui 8 | ui/entity-list.ui 9 | ui/entity-row.ui 10 | ui/group-detail.ui 11 | ui/groups.ui 12 | ui/main.ui 13 | ui/menu.ui 14 | ui/new-group.ui 15 | ui/setup.ui 16 | 17 | 18 | -------------------------------------------------------------------------------- /data/ui/about.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | False 5 | dialog 6 | Luminance 7 | Copyright © 2016 Craig Cabrey 8 | https://craigcabrey.github.io/luminance/ 9 | Craig Cabrey 10 | gpl-2-0 11 | 12 | 13 | False 14 | vertical 15 | 2 16 | 17 | 18 | False 19 | end 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | False 29 | False 30 | 0 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /data/ui/bridge.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | False 6 | 50 7 | 50 8 | 12 9 | 12 10 | vertical 11 | 12 | 13 | True 14 | False 15 | in 16 | 12 17 | 12 18 | 0.45 19 | none 20 | 21 | 22 | True 23 | False 24 | 0 25 | Bridge Information 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | True 34 | False 35 | 6 36 | 6 37 | in 38 | 39 | 40 | True 41 | False 42 | none 43 | 44 | 45 | True 46 | False 47 | False 48 | 49 | 50 | True 51 | False 52 | 12 53 | 6 54 | 8 55 | 8 56 | center 57 | 58 | 59 | True 60 | False 61 | 0 62 | end 63 | Name 64 | True 65 | 66 | 67 | True 68 | True 69 | start 70 | 0 71 | 72 | 73 | 74 | 75 | True 76 | False 77 | 78 | True 79 | 80 | 81 | False 82 | False 83 | end 84 | 1 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | True 94 | False 95 | False 96 | 97 | 98 | True 99 | False 100 | 12 101 | 6 102 | 8 103 | 8 104 | center 105 | 106 | 107 | True 108 | False 109 | 0 110 | end 111 | Identifier 112 | True 113 | 114 | 115 | True 116 | True 117 | start 118 | 0 119 | 120 | 121 | 122 | 123 | True 124 | False 125 | 126 | True 127 | 128 | 129 | False 130 | False 131 | end 132 | 1 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | True 142 | False 143 | False 144 | 145 | 146 | True 147 | False 148 | 12 149 | 6 150 | 8 151 | 8 152 | center 153 | 154 | 155 | True 156 | False 157 | 0 158 | end 159 | Model 160 | True 161 | 162 | 163 | True 164 | True 165 | start 166 | 0 167 | 168 | 169 | 170 | 171 | True 172 | False 173 | 174 | True 175 | 176 | 177 | False 178 | False 179 | end 180 | 1 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | True 190 | False 191 | False 192 | 193 | 194 | True 195 | False 196 | 12 197 | 6 198 | 8 199 | 8 200 | center 201 | 202 | 203 | True 204 | False 205 | 0 206 | end 207 | Timezone 208 | True 209 | 210 | 211 | True 212 | True 213 | start 214 | 0 215 | 216 | 217 | 218 | 219 | True 220 | False 221 | 222 | True 223 | 224 | 225 | False 226 | False 227 | end 228 | 1 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | True 244 | False 245 | in 246 | 12 247 | 12 248 | 0.45 249 | none 250 | 251 | 252 | True 253 | False 254 | 0 255 | Network Information 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | True 264 | False 265 | 6 266 | 6 267 | in 268 | 269 | 270 | True 271 | False 272 | none 273 | 274 | 275 | True 276 | False 277 | False 278 | 279 | 280 | True 281 | False 282 | 12 283 | 6 284 | 8 285 | 8 286 | center 287 | 288 | 289 | True 290 | False 291 | 0 292 | end 293 | Address 294 | True 295 | 296 | 297 | True 298 | True 299 | start 300 | 0 301 | 302 | 303 | 304 | 305 | True 306 | False 307 | 0 308 | end 309 | 310 | True 311 | 312 | 313 | False 314 | False 315 | end 316 | 1 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | True 326 | False 327 | False 328 | 329 | 330 | True 331 | False 332 | 12 333 | 6 334 | 8 335 | 8 336 | center 337 | 338 | 339 | True 340 | False 341 | 0 342 | end 343 | Gateway / Netmask 344 | True 345 | 346 | 347 | True 348 | True 349 | start 350 | 0 351 | 352 | 353 | 354 | 355 | True 356 | False 357 | 0 358 | end 359 | 360 | True 361 | 362 | 363 | False 364 | False 365 | end 366 | 1 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | True 376 | False 377 | False 378 | 379 | 380 | True 381 | False 382 | 12 383 | 6 384 | 8 385 | 8 386 | center 387 | 388 | 389 | True 390 | False 391 | 0 392 | end 393 | MAC Address 394 | True 395 | 396 | 397 | True 398 | True 399 | start 400 | 0 401 | 402 | 403 | 404 | 405 | True 406 | False 407 | 408 | True 409 | 410 | 411 | False 412 | False 413 | end 414 | 1 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | True 424 | False 425 | False 426 | 427 | 428 | True 429 | False 430 | 12 431 | 6 432 | 8 433 | 8 434 | center 435 | 436 | 437 | True 438 | False 439 | 0 440 | end 441 | DHCP 442 | True 443 | 444 | 445 | True 446 | True 447 | start 448 | 0 449 | 450 | 451 | 452 | 453 | True 454 | False 455 | 456 | True 457 | 458 | 459 | False 460 | False 461 | end 462 | 1 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | False 478 | False 479 | 12 480 | 12 481 | none 482 | 0.45 483 | 484 | 485 | True 486 | False 487 | 0 488 | Software Information 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | True 497 | False 498 | 6 499 | 6 500 | in 501 | 502 | 503 | True 504 | False 505 | none 506 | 507 | 508 | True 509 | False 510 | False 511 | 512 | 513 | True 514 | False 515 | 12 516 | 6 517 | 8 518 | 8 519 | center 520 | 521 | 522 | True 523 | False 524 | 0 525 | end 526 | Version 527 | True 528 | 529 | 530 | True 531 | True 532 | start 533 | 0 534 | 535 | 536 | 537 | 538 | True 539 | False 540 | 541 | True 542 | 543 | 544 | False 545 | False 546 | end 547 | 1 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | True 557 | False 558 | False 559 | 560 | 561 | True 562 | False 563 | 12 564 | 6 565 | 8 566 | 8 567 | center 568 | 569 | 570 | True 571 | False 572 | 0 573 | end 574 | API Version 575 | True 576 | 577 | 578 | True 579 | True 580 | start 581 | 0 582 | 583 | 584 | 585 | 586 | True 587 | False 588 | 589 | True 590 | 591 | 592 | False 593 | False 594 | end 595 | 1 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | True 605 | False 606 | False 607 | 608 | 609 | True 610 | False 611 | 0 612 | 16 613 | 12 614 | 6 615 | 6 616 | 6 617 | center 618 | 619 | 620 | True 621 | False 622 | True 623 | True 624 | center 625 | 0 626 | Actions 627 | True 628 | 629 | 630 | 0 631 | 0 632 | 1 633 | 1 634 | 635 | 636 | 637 | 638 | Check for updates 639 | True 640 | True 641 | end 642 | center 643 | 644 | 645 | 646 | 2 647 | 0 648 | 1 649 | 2 650 | 651 | 652 | 653 | 654 | Rescan Lights 655 | True 656 | True 657 | end 658 | center 659 | 660 | 661 | 662 | 1 663 | 0 664 | 1 665 | 2 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | -------------------------------------------------------------------------------- /data/ui/entity-detail.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 255 6 | 1 7 | 8 | 9 | True 10 | False 11 | 6 12 | True 13 | 14 | 15 | True 16 | True 17 | True 18 | Close 19 | 20 | 21 | 22 | 23 | 24 | Save 25 | True 26 | True 27 | True 28 | 29 | 30 | 31 | end 32 | 1 33 | 34 | 35 | 36 | 37 | True 38 | True 39 | none 40 | never 41 | 400 42 | 43 | 44 | True 45 | False 46 | 50 47 | 50 48 | 12 49 | 12 50 | vertical 51 | 52 | 53 | True 54 | False 55 | 6 56 | horizontal 57 | 58 | 59 | True 60 | False 61 | Name 62 | right 63 | end 64 | center 65 | 66 | 67 | start 68 | 0 69 | 70 | 71 | 72 | 73 | True 74 | True 75 | True 76 | False 77 | 78 | 79 | start 80 | 1 81 | 82 | 83 | 84 | 85 | 86 | 87 | True 88 | False 89 | in 90 | 12 91 | 12 92 | 0.45 93 | none 94 | 95 | 96 | True 97 | False 98 | 6 99 | 6 100 | in 101 | 102 | 103 | True 104 | False 105 | none 106 | 107 | 108 | True 109 | False 110 | False 111 | 112 | 113 | True 114 | False 115 | 12 116 | 6 117 | 8 118 | 8 119 | center 120 | 6 121 | 122 | 123 | brightness-scale-adjustment 124 | 0 125 | True 126 | True 127 | False 128 | horizontal 129 | 130 | 131 | 132 | True 133 | True 134 | 0 135 | 136 | 137 | 138 | 139 | True 140 | True 141 | 142 | 143 | 144 | False 145 | True 146 | 1 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | True 156 | False 157 | False 158 | 159 | 160 | True 161 | False 162 | 12 163 | 6 164 | 8 165 | 8 166 | center 167 | 6 168 | 169 | 170 | True 171 | False 172 | rgb(85,87,83) 173 | True 174 | False 175 | 176 | 177 | 178 | True 179 | True 180 | 0 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | True 196 | False 197 | in 198 | 12 199 | 12 200 | 0.45 201 | none 202 | 203 | 204 | True 205 | False 206 | 0 207 | Special Effects 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | True 216 | False 217 | 6 218 | 6 219 | in 220 | 221 | 222 | True 223 | False 224 | none 225 | 226 | 227 | True 228 | False 229 | False 230 | 231 | 232 | True 233 | False 234 | 12 235 | 6 236 | 8 237 | 8 238 | center 239 | 240 | 241 | True 242 | False 243 | 0 244 | left 245 | center 246 | Loop Colors 247 | True 248 | 249 | 250 | True 251 | True 252 | start 253 | 0 254 | 255 | 256 | 257 | 258 | True 259 | True 260 | 261 | 262 | 263 | False 264 | True 265 | 1 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | True 275 | False 276 | False 277 | 278 | 279 | True 280 | False 281 | 6 282 | 12 283 | 6 284 | 8 285 | 8 286 | center 287 | 288 | 289 | True 290 | False 291 | 0 292 | left 293 | center 294 | Alert 295 | True 296 | 297 | 298 | True 299 | True 300 | start 301 | 0 302 | 303 | 304 | 305 | 306 | True 307 | True 308 | Short 309 | 310 | 311 | 312 | False 313 | True 314 | 1 315 | 316 | 317 | 318 | 319 | True 320 | True 321 | Long 322 | 323 | 324 | 325 | False 326 | True 327 | 1 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | -------------------------------------------------------------------------------- /data/ui/entity-list.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | False 6 | 50 7 | 50 8 | 12 9 | 12 10 | vertical 11 | 12 | 13 | False 14 | False 15 | 12 16 | 12 17 | none 18 | 0.45 19 | 20 | 21 | True 22 | False 23 | 6 24 | 12 25 | in 26 | 27 | 28 | True 29 | True 30 | none 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /data/ui/entity-row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 255 6 | 1 7 | 8 | 9 | False 10 | 11 | 12 | True 13 | False 14 | 0 15 | none 16 | 17 | 18 | True 19 | False 20 | 1 21 | 1 22 | 6 23 | 6 24 | 0 25 | 0 26 | 27 | 28 | True 29 | False 30 | rgb(85,87,83) 31 | True 32 | False 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | True 43 | False 44 | 6 45 | 6 46 | 6 47 | 6 48 | 6 49 | True 50 | 51 | 52 | True 53 | False 54 | True 55 | 0 56 | 15 57 | 15 58 | entity-switch 59 | end 60 | 61 | 62 | False 63 | False 64 | 65 | 66 | 67 | 68 | brightness-scale-adjustment 69 | 0 70 | True 71 | True 72 | False 73 | horizontal 74 | True 75 | 76 | 77 | 78 | True 79 | True 80 | 81 | 82 | 83 | 84 | True 85 | True 86 | True 87 | color-chooser-popover 88 | 89 | 90 | True 91 | False 92 | applications-graphics 93 | 94 | 95 | 96 | 97 | False 98 | False 99 | 100 | 101 | 102 | 103 | True 104 | True 105 | end 106 | 107 | 108 | 109 | False 110 | False 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /data/ui/group-detail.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | False 6 | 6 7 | 6 8 | none 9 | 10 | 11 | True 12 | False 13 | 0 14 | Lights 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | True 23 | False 24 | 6 25 | 6 26 | in 27 | 28 | 29 | True 30 | False 31 | none 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /data/ui/groups.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | False 6 | 50 7 | 50 8 | 12 9 | 12 10 | vertical 11 | 12 | 13 | False 14 | False 15 | 12 16 | 12 17 | none 18 | 0.45 19 | 20 | 21 | vertical 22 | 6 23 | 12 24 | 25 | 26 | True 27 | False 28 | in 29 | 30 | 31 | True 32 | True 33 | none 34 | 35 | 36 | 37 | 38 | 39 | start 40 | 41 | 42 | 43 | 44 | True 45 | False 46 | icons 47 | 1 48 | 51 | 52 | 53 | True 54 | False 55 | False 56 | list-add-symbolic 57 | Add Group 58 | 59 | 60 | 61 | 62 | 63 | end 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /data/ui/main.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | False 6 | vertical 7 | top 8 | 9 | 10 | True 11 | False 12 | 13 | 14 | True 15 | True 16 | none 17 | never 18 | 460 19 | 20 | 21 | lights 22 | Lights 23 | 0 24 | 25 | 26 | 27 | 28 | True 29 | False 30 | none 31 | never 32 | 460 33 | 34 | 35 | groups 36 | Groups 37 | 1 38 | 39 | 40 | 41 | 42 | True 43 | True 44 | none 45 | never 46 | 460 47 | 48 | 49 | bridge 50 | Bridge 51 | 2 52 | 53 | 54 | 55 | 56 | True 57 | True 58 | 0 59 | 60 | 61 | 62 | 63 | status-bar 64 | True 65 | False 66 | 10 67 | 10 68 | True 69 | 2 70 | 71 | 72 | False 73 | True 74 | end 75 | 1 76 | 77 | 78 | 79 | 80 | True 81 | False 82 | 6 83 | True 84 | 85 | 86 | True 87 | False 88 | main-stack 89 | 90 | 91 | 92 | 93 | True 94 | True 95 | True 96 | none 97 | menu-contents 98 | 99 | 100 | True 101 | False 102 | open-menu-symbolic 103 | 104 | 105 | 106 | 107 | end 108 | 0 109 | 110 | 111 | 112 | 113 | False 114 | headerbar 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /data/ui/menu.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | app.connect 7 | Connect... 8 | 9 |
10 |
11 | 12 | app.about 13 | _About 14 | 15 | 16 | app.quit 17 | _Quit 18 | <Primary>q 19 | 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /data/ui/new-group.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | True 6 | none 7 | never 8 | 400 9 | 430 10 | 11 | 12 | True 13 | False 14 | 50 15 | 50 16 | 12 17 | 12 18 | vertical 19 | 6 20 | 21 | 22 | True 23 | False 24 | 6 25 | horizontal 26 | 27 | 28 | True 29 | False 30 | Name 31 | right 32 | end 33 | center 34 | 35 | 36 | start 37 | 0 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | True 47 | True 48 | True 49 | False 50 | name-buffer 51 | 52 | 53 | start 54 | 1 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /data/ui/setup.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | False 6 | vertical 7 | 8 | 9 | True 10 | False 11 | vertical 12 | center 13 | true 14 | center 15 | true 16 | 17 | 18 | 50 19 | 50 20 | True 21 | False 22 | True 23 | 24 | 25 | False 26 | True 27 | 0 28 | 29 | 30 | 31 | 32 | True 33 | False 34 | Searching… 35 | 36 | 37 | False 38 | True 39 | 1 40 | 41 | 42 | 43 | 44 | False 45 | True 46 | 0 47 | 48 | 49 | 50 | 51 | True 52 | False 53 | True 54 | 55 | 56 | True 57 | True 58 | never 59 | none 60 | 61 | 62 | True 63 | False 64 | 50 65 | 50 66 | 12 67 | 12 68 | vertical 69 | 70 | 71 | True 72 | False 73 | in 74 | 12 75 | 12 76 | 0.45 77 | none 78 | 79 | 80 | True 81 | False 82 | 0 83 | Select the desired bridge: 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | True 92 | False 93 | 6 94 | 12 95 | in 96 | 97 | 98 | True 99 | True 100 | single 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | results 113 | Select Bridge 114 | 2 115 | 116 | 117 | 118 | 119 | False 120 | center 121 | 10 122 | 12 123 | top 124 | 125 | 126 | True 127 | False 128 | Bridge Address 129 | 130 | 131 | False 132 | True 133 | 0 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | True 143 | True 144 | center 145 | manual-address-buffer 146 | 147 | 148 | False 149 | True 150 | 1 151 | 152 | 153 | 154 | 155 | manual 156 | Manual Setup 157 | 2 158 | 159 | 160 | 161 | 162 | True 163 | False 164 | center 165 | vertical 166 | 167 | 168 | True 169 | False 170 | center 171 | center 172 | True 173 | 10 174 | 175 | 176 | True 177 | False 178 | start 179 | gtk-dialog-warning 180 | 6 181 | 182 | 183 | False 184 | True 185 | 0 186 | 187 | 188 | 189 | 190 | True 191 | False 192 | No bridges could be found. 193 | 194 | Check your computer and/or 195 | bridge network connectivity. 196 | 197 | 198 | False 199 | True 200 | 1 201 | 202 | 203 | 204 | 205 | False 206 | True 207 | 0 208 | 209 | 210 | 211 | 212 | Manual Setup 213 | True 214 | True 215 | True 216 | center 217 | end 218 | 219 | 220 | 221 | False 222 | False 223 | 20 224 | end 225 | 1 226 | 227 | 228 | 229 | 230 | no-bridges-found 231 | No bridges found 232 | 3 233 | 234 | 235 | 236 | 237 | True 238 | False 239 | True 240 | 241 | 242 | True 243 | False 244 | vertical 245 | center 246 | true 247 | center 248 | true 249 | 250 | 251 | True 252 | False 253 | /com/craigcabrey/luminance/icons/link_button.svg 254 | 255 | 256 | False 257 | True 258 | 0 259 | 260 | 261 | 262 | 263 | True 264 | False 265 | Press link button to continue. 266 | 267 | 268 | False 269 | True 270 | 1 271 | 272 | 273 | 274 | 275 | press-link-button 276 | Press Link Button 277 | 0 278 | 279 | 280 | 281 | 282 | True 283 | False 284 | center 285 | vertical 286 | 287 | 288 | True 289 | False 290 | center 291 | center 292 | True 293 | 10 294 | 295 | 296 | True 297 | False 298 | start 299 | gtk-dialog-warning 300 | 6 301 | 302 | 303 | False 304 | True 305 | 0 306 | 307 | 308 | 309 | 310 | True 311 | False 312 | An unknown error occurred while 313 | connecting to the bridge. 314 | 315 | 316 | 317 | False 318 | True 319 | 1 320 | 321 | 322 | 323 | 324 | False 325 | True 326 | 0 327 | 328 | 329 | 330 | 331 | unknown-error 332 | Unknown Error 333 | 1 334 | 335 | 336 | 337 | 338 | -------------------------------------------------------------------------------- /luminance/Makefile.am: -------------------------------------------------------------------------------- 1 | SUBDIRS = views 2 | 3 | BUILT_SOURCES = __init__.py 4 | EXTRA_DIST = __init__.py.in 5 | 6 | luminancedir = $(pkgpythondir) 7 | 8 | luminance_PYTHON = application.py \ 9 | __init__.py 10 | 11 | __init__.py: __init__.py.in 12 | $(SED) \ 13 | -e 's|@datadir[@]|$(pkgdatadir)|g' \ 14 | -e 's|@version[@]|$(VERSION)|g' \ 15 | < $< > $@ 16 | -------------------------------------------------------------------------------- /luminance/__init__.py.in: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from gi.repository import Gio 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | RESOURCE_PREFIX = '/com/craigcabrey/luminance' 9 | __version__ = '@version@' 10 | 11 | if os.path.exists(os.path.join(here, '../data')): 12 | print('Using local data') 13 | 14 | DATA_DIR = os.path.abspath(os.path.join(here, '../data')) 15 | settings = Gio.Settings.new_full( 16 | Gio.SettingsSchemaSource.new_from_directory( 17 | DATA_DIR, 18 | Gio.SettingsSchemaSource.get_default(), 19 | True 20 | ).lookup( 21 | 'com.craigcabrey.luminance', 22 | False 23 | ), 24 | None, 25 | None 26 | ) 27 | else: 28 | DATA_DIR = '@datadir@' 29 | settings = Gio.Settings('com.craigcabrey.luminance') 30 | 31 | Gio.Resource._register( 32 | Gio.Resource.load( 33 | os.path.abspath( 34 | os.path.join( 35 | DATA_DIR, 36 | 'resources.gresource' 37 | ) 38 | ) 39 | ) 40 | ) 41 | 42 | def get_resource_path(resource): 43 | return '{prefix}/{resource}'.format( 44 | prefix=RESOURCE_PREFIX, resource=resource 45 | ) 46 | 47 | del here 48 | del Gio 49 | del os 50 | del sys 51 | -------------------------------------------------------------------------------- /luminance/application.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import gi 4 | 5 | gi.require_version('Gtk', '3.0') 6 | 7 | from gi.repository import Gio 8 | from gi.repository import GLib 9 | from gi.repository import Gtk 10 | 11 | import phue 12 | 13 | from . import __version__ 14 | from . import get_resource_path 15 | from . import settings 16 | from .views.setup import Setup 17 | from .views.window import Window 18 | 19 | 20 | class Application(Gtk.Application): 21 | def __init__(self, *args, **kwargs): 22 | super().__init__( 23 | *args, 24 | application_id='com.craigcabrey.luminance', 25 | flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, 26 | **kwargs 27 | ) 28 | 29 | self.add_main_option( 30 | 'host', 31 | ord('h'), 32 | GLib.OptionFlags.NONE, 33 | GLib.OptionArg.STRING, 34 | 'Host to use for bridge connection', 35 | 'HOST' 36 | ) 37 | 38 | self.add_main_option( 39 | 'username', 40 | ord('u'), 41 | GLib.OptionFlags.NONE, 42 | GLib.OptionArg.STRING, 43 | 'Username to use for bridge connection', 44 | 'USERNAME' 45 | ) 46 | 47 | self.bridge = None 48 | self.host = settings.get_string('host') 49 | self.username = settings.get_string('username') 50 | self.window = None 51 | 52 | def do_startup(self): 53 | Gtk.Application.do_startup(self) 54 | 55 | builder = Gtk.Builder() 56 | 57 | builder.add_from_resource(get_resource_path('ui/about.ui')) 58 | builder.add_from_resource(get_resource_path('ui/menu.ui')) 59 | 60 | self.app_menu = builder.get_object('app-menu') 61 | self.about_dialog = builder.get_object('about-dialog') 62 | 63 | action = Gio.SimpleAction.new('connect', None) 64 | action.connect('activate', self._on_connect) 65 | self.add_action(action) 66 | 67 | action = Gio.SimpleAction.new('about', None) 68 | action.connect('activate', self._on_about) 69 | self.add_action(action) 70 | 71 | action = Gio.SimpleAction.new('quit', None) 72 | action.connect('activate', self._on_quit) 73 | self.add_action(action) 74 | 75 | self.set_app_menu(self.app_menu) 76 | 77 | self.mark_busy() 78 | 79 | def do_activate(self): 80 | Gtk.Application.do_activate(self) 81 | 82 | self._connect(self.host, self.username) 83 | 84 | self.unmark_busy() 85 | 86 | def do_command_line(self, command_line): 87 | options = command_line.get_options_dict() 88 | 89 | if options.contains('host'): 90 | self.host = str(options.lookup_value('host')) 91 | 92 | if options.contains('username'): 93 | self.username = str(options.lookup_value('username')) 94 | 95 | self.activate() 96 | 97 | return 0 98 | 99 | def _connect(self, host, username): 100 | if host and username: 101 | self.bridge = phue.Bridge( 102 | self.host, 103 | username=username 104 | ) 105 | 106 | self._init() 107 | else: 108 | self._setup() 109 | 110 | def _init(self): 111 | if not self.window: 112 | self.window = Window( 113 | self.bridge, 114 | application=self 115 | ) 116 | else: 117 | self.window.reload(self.bridge) 118 | 119 | self.window.show_all() 120 | self.window.present() 121 | 122 | def _setup(self): 123 | setup = Setup(application=self) 124 | setup.connect('cancel', self._on_quit) 125 | setup.connect('apply', self._setup_finished) 126 | setup.show_all() 127 | setup.present() 128 | 129 | def _setup_finished(self, setup): 130 | self.bridge = setup.bridge 131 | setup.hide() 132 | setup.destroy() 133 | self._init() 134 | 135 | def _on_connect(self, *args): 136 | if self.window: 137 | self.window.hide() 138 | self.window.destroy() 139 | 140 | self._setup() 141 | 142 | def _on_about(self, *args): 143 | self.about_dialog.set_logo_icon_name('luminance') 144 | self.about_dialog.set_version(__version__) 145 | self.about_dialog.set_transient_for(self.window) 146 | self.about_dialog.set_modal(True) 147 | self.about_dialog.present() 148 | 149 | def _on_quit(self, *args): 150 | if self.bridge: 151 | settings.set_string('host', self.bridge.ip) 152 | settings.set_string('username', self.bridge.username) 153 | 154 | self.quit() 155 | -------------------------------------------------------------------------------- /luminance/views/Makefile.am: -------------------------------------------------------------------------------- 1 | views_PYTHON = bridge.py \ 2 | entity.py \ 3 | group.py \ 4 | groups.py \ 5 | light.py \ 6 | setup.py \ 7 | util.py \ 8 | window.py 9 | 10 | viewsdir = $(pkgpythondir)/views 11 | -------------------------------------------------------------------------------- /luminance/views/bridge.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gtk', '3.0') 4 | 5 | from gi.repository import Gtk 6 | 7 | from .. import get_resource_path 8 | 9 | 10 | class Bridge(Gtk.Box): 11 | def __init__(self, bridge, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | 14 | self.api = bridge.get_api() 15 | 16 | builder = Gtk.Builder() 17 | builder.add_from_resource(get_resource_path('ui/bridge.ui')) 18 | builder.connect_signals(self) 19 | 20 | self.content = builder.get_object('bridge-page-content') 21 | 22 | self.bridge_name_label = builder.get_object('bridge-name') 23 | self.bridge_name_label.set_text(bridge.name) 24 | 25 | self.bridge_id_label = builder.get_object('bridge-id') 26 | self.bridge_id_label.set_text(self.api['config']['bridgeid']) 27 | 28 | self.bridge_model_label = builder.get_object('bridge-model') 29 | self.bridge_model_label.set_text(self.api['config']['modelid']) 30 | 31 | self.bridge_timezone_label = builder.get_object('bridge-timezone') 32 | self.bridge_timezone_label.set_text(self.api['config']['timezone']) 33 | 34 | self.bridge_address_label = builder.get_object('bridge-address') 35 | if bridge.ip == self.api['config']['ipaddress']: 36 | self.bridge_address_label.set_text(bridge.ip) 37 | else: 38 | self.bridge_address_label.set_text( 39 | '{host} ({ip})'.format( 40 | host=bridge.ip, 41 | ip=self.api['config']['ipaddress'] 42 | ) 43 | ) 44 | 45 | self.network_connection_label = builder.get_object('connection-details') 46 | self.network_connection_label .set_text( 47 | '{gw} / {nm}'.format( 48 | gw=self.api['config']['gateway'], 49 | nm=self.api['config']['netmask'] 50 | ) 51 | ) 52 | 53 | self.bridge_mac_address_label = builder.get_object('bridge-mac-address') 54 | self.bridge_mac_address_label.set_text(self.api['config']['mac']) 55 | 56 | self.bridge_dhcp_label = builder.get_object('bridge-dhcp') 57 | self.bridge_dhcp_label.set_text( 58 | 'Yes' if self.api['config']['dhcp'] else 'No' 59 | ) 60 | 61 | self.software_version_label = builder.get_object('software-version') 62 | self.software_version_label.set_text(self.api['config']['swversion']) 63 | 64 | self.api_version_label = builder.get_object('api-version') 65 | self.api_version_label.set_text(self.api['config']['apiversion']) 66 | 67 | self.add(self.content) 68 | 69 | def _on_rescan_clicked(self, *args): 70 | print('not implemented') 71 | 72 | def _on_update_clicked(self, *args): 73 | print('not implemented') 74 | -------------------------------------------------------------------------------- /luminance/views/entity.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | import gi 4 | 5 | gi.require_version('Gdk', '3.0') 6 | gi.require_version('Gtk', '3.0') 7 | 8 | from gi.repository import Gdk 9 | from gi.repository import Gtk 10 | 11 | from .util import hsv_to_gdk_rgb 12 | from .. import get_resource_path 13 | 14 | 15 | class FramedEntityList(Gtk.Box): 16 | def __init__(self, model, *args, **kwargs): 17 | super().__init__(*args, **kwargs) 18 | 19 | builder = Gtk.Builder() 20 | builder.add_from_resource(get_resource_path('ui/entity-list.ui')) 21 | builder.connect_signals(self) 22 | 23 | self.content = builder.get_object('content-wrapper') 24 | self.list = builder.get_object('list') 25 | 26 | for entity in model: 27 | self.list.add(ListBoxRow(entity)) 28 | 29 | self.add(self.content) 30 | 31 | def _on_row_activated(self, listbox, row): 32 | DetailWindow( 33 | row.model, 34 | modal=True, 35 | transient_for=self.get_toplevel(), 36 | type_hint=Gdk.WindowTypeHint.DIALOG 37 | ).present() 38 | 39 | 40 | class ListBoxRow(Gtk.ListBoxRow): 41 | def __init__(self, model, *args, **kwargs): 42 | super().__init__(*args, **kwargs) 43 | 44 | self._model = model 45 | 46 | builder = Gtk.Builder() 47 | builder.add_from_resource(get_resource_path('ui/entity-row.ui')) 48 | builder.connect_signals(self) 49 | 50 | entity_name_label = builder.get_object('entity-name-label') 51 | 52 | if hasattr(self.model, 'group_id') and self.model.group_id == 0: 53 | entity_name_label.set_text('All Lights') 54 | else: 55 | entity_name_label.set_text(self.model.name) 56 | 57 | self.color_chooser = builder.get_object('color-chooser') 58 | 59 | if self.model.on: 60 | self.color_chooser.set_rgba( 61 | hsv_to_gdk_rgb( 62 | self.model.hue, 63 | self.model.saturation, 64 | self.model.brightness 65 | ) 66 | ) 67 | 68 | self.brightness_scale = builder.get_object('brightness-scale') 69 | self.brightness_scale.set_value(self.model.brightness) 70 | 71 | self.color_chooser_popover_button = builder.get_object('color-chooser-popover-button') 72 | 73 | entity_switch = builder.get_object('entity-switch') 74 | entity_switch.set_state(self.model.on) 75 | entity_switch.emit('state-set', self.model.on) 76 | 77 | content = builder.get_object('content-wrapper') 78 | 79 | self.add(content) 80 | 81 | @property 82 | def model(self): 83 | return self._model 84 | 85 | def _on_color_activate(self, *args): 86 | if self.model.on and self.color_chooser.get_visible(): 87 | rgba = self.color_chooser.get_rgba() 88 | hsv = colorsys.rgb_to_hsv(rgba.red, rgba.green, rgba.blue) 89 | 90 | self.model.hue = int(hsv[0] * 65535) 91 | self.model.saturation = int(hsv[1] * 255) 92 | 93 | def _on_brightness_scale_change(self, scale, delta, value): 94 | value = max(min(int(value), 255), 1) 95 | 96 | if value != self.model.brightness: 97 | self.model.brightness = value 98 | 99 | def _on_entity_switch_state_set(self, switch, value): 100 | self.model.on = value 101 | self.brightness_scale.set_sensitive(value) 102 | self.color_chooser_popover_button.set_sensitive(value) 103 | 104 | 105 | class DetailWindow(Gtk.Window): 106 | def __init__(self, model, *args, id=None, **kwargs): 107 | super().__init__(*args, **kwargs) 108 | 109 | self._model = model 110 | 111 | builder = Gtk.Builder() 112 | builder.add_from_resource(get_resource_path('ui/entity-detail.ui')) 113 | builder.connect_signals(self) 114 | 115 | self.headerbar = builder.get_object('headerbar') 116 | self.headerbar.set_title(model.name) 117 | 118 | self.name_entry_container = builder.get_object('name-entry-container') 119 | self.name_entry = builder.get_object('name-entry') 120 | self.name_entry.set_text(self.model.name) 121 | 122 | self.brightness_scale = builder.get_object('brightness-scale') 123 | self.brightness_scale.set_value(self.model.brightness) 124 | 125 | self.color_chooser = builder.get_object('color-chooser') 126 | self.color_chooser.set_rgba( 127 | hsv_to_gdk_rgb( 128 | self.model.hue, 129 | self.model.saturation, 130 | self.model.brightness 131 | ) 132 | ) 133 | 134 | self.alert_long_button = builder.get_object('alert-long-button') 135 | self.alert_short_button = builder.get_object('alert-short-button') 136 | 137 | self.colorloop_switch = builder.get_object('colorloop-switch') 138 | self.colorloop_switch.set_state(self.model.effect == 'colorloop') 139 | 140 | self.entity_switch = builder.get_object('entity-switch') 141 | self.entity_switch.set_state(self.model.on) 142 | self.entity_switch.emit('state-set', self.model.on) 143 | 144 | self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) 145 | self.set_titlebar(self.headerbar) 146 | 147 | self.content = builder.get_object('content') 148 | 149 | wrapper = builder.get_object('content-wrapper') 150 | 151 | self.add(wrapper) 152 | 153 | @property 154 | def model(self): 155 | return self._model 156 | 157 | def _on_alert_long_click(self, *args): 158 | self.model.alert = 'none' 159 | self.model.alert = 'lselect' 160 | 161 | def _on_alert_short_click(self, *args): 162 | self.model.alert = 'none' 163 | self.model.alert = 'select' 164 | 165 | def _on_brightness_scale_change(self, scale, delta, value): 166 | value = max(min(int(value), 255), 1) 167 | 168 | if value != self.model.brightness: 169 | self.model.brightness = value 170 | 171 | def _on_close_click(self, *args): 172 | self.destroy() 173 | 174 | def _on_color_activate(self, *args): 175 | if self.model.on and self.color_chooser.get_visible(): 176 | rgba = self.color_chooser.get_rgba() 177 | hsv = colorsys.rgb_to_hsv(rgba.red, rgba.green, rgba.blue) 178 | 179 | self.model.hue = int(hsv[0] * 65535) 180 | self.model.saturation = int(hsv[1] * 255) 181 | 182 | def _on_save_click(self, *args): 183 | new_name = self.name_entry.get_text() 184 | 185 | if new_name != self.model.name: 186 | self.model.name = new_name 187 | 188 | self.destroy() 189 | 190 | def _on_colorloop_switch_change(self, switch, value): 191 | if value: 192 | self.model.effect = 'colorloop' 193 | self.color_chooser.set_sensitive(False) 194 | else: 195 | self.model.effect = 'none' 196 | self.color_chooser.set_sensitive(True) 197 | 198 | def _on_entity_switch_change(self, switch, value): 199 | self.model.on = value 200 | 201 | self.alert_long_button.set_sensitive(value) 202 | self.alert_short_button.set_sensitive(value) 203 | self.brightness_scale.set_sensitive(value) 204 | self.colorloop_switch.set_sensitive(value) 205 | 206 | if value: 207 | self.color_chooser.set_sensitive(not self.colorloop_switch.get_state()) 208 | else: 209 | self.colorloop_switch.set_state(False) 210 | self.color_chooser.set_sensitive(False) 211 | -------------------------------------------------------------------------------- /luminance/views/group.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | import gi 4 | 5 | gi.require_version('Gtk', '3.0') 6 | 7 | from gi.repository import Gtk 8 | 9 | from .entity import DetailWindow 10 | from .util import hsv_to_gdk_rgb 11 | from .. import get_resource_path 12 | 13 | 14 | class NewGroup(Gtk.Dialog): 15 | def __init__(self, bridge, *args, **kwargs): 16 | super().__init__( 17 | *args, 18 | modal=True, 19 | use_header_bar=1, 20 | title='New Group', 21 | **kwargs 22 | ) 23 | 24 | self._name = None 25 | self._lights = [] 26 | self.add_button('Cancel', Gtk.ResponseType.CANCEL) 27 | self.add_button('Save', Gtk.ResponseType.APPLY) 28 | self.set_response_sensitive(Gtk.ResponseType.APPLY, bool(self.name)) 29 | self.connect('response', self._on_response) 30 | 31 | builder = Gtk.Builder() 32 | builder.add_from_resource(get_resource_path('ui/new-group.ui')) 33 | builder.connect_signals(self) 34 | 35 | content_wrapper = builder.get_object('content-wrapper') 36 | content = builder.get_object('content') 37 | 38 | self.lights_list = SelectableLightList( 39 | bridge.lights_by_id.values(), 40 | set() 41 | ) 42 | 43 | content.add(self.lights_list) 44 | 45 | content_area = self.get_content_area() 46 | content_area.add(content_wrapper) 47 | 48 | @property 49 | def name(self): 50 | return self._name 51 | 52 | @name.setter 53 | def name(self, value): 54 | self._name = value 55 | 56 | @property 57 | def lights(self): 58 | return self._lights 59 | 60 | @lights.setter 61 | def lights(self, value): 62 | self._lights = value 63 | 64 | def _on_name_changed(self, name_buffer, *args): 65 | self.name = name_buffer.get_text() 66 | self.set_response_sensitive(Gtk.ResponseType.APPLY, bool(self.name)) 67 | 68 | def _on_response(self, *args): 69 | self.lights = list(self.lights_list.selected_lights) 70 | 71 | 72 | class GroupDetail(DetailWindow): 73 | def __init__(self, *args, **kwargs): 74 | super().__init__(*args, **kwargs) 75 | 76 | self.headerbar.set_subtitle('Group {id}'.format(id=self.model.group_id)) 77 | 78 | delete_button = Gtk.Button(label='Delete', visible=True) 79 | delete_button.connect('clicked', self._on_delete_click) 80 | 81 | self.headerbar.pack_end(delete_button) 82 | 83 | self.lights_list = SelectableLightList( 84 | self.model.bridge.lights_by_id.values(), 85 | set(light.light_id for light in self.model.lights) 86 | ) 87 | 88 | self.content.pack_start(self.lights_list, True, True, 6) 89 | self.content.reorder_child(self.lights_list, 2) 90 | 91 | def _on_delete_click(self, *args): 92 | dialog = Gtk.MessageDialog( 93 | buttons=Gtk.ButtonsType.YES_NO, 94 | secondary_text='Are you sure you wish to delete this group?', 95 | text='Confirm Deletion', 96 | transient_for=self 97 | ) 98 | 99 | if dialog.run(): 100 | self.model.bridge.delete_group(self.model.group_id) 101 | self.destroy() 102 | 103 | dialog.destroy() 104 | 105 | def _on_save_click(self, *args): 106 | self.model.lights = list(self.lights_list.selected_lights) 107 | 108 | super()._on_save_click(self, *args) 109 | 110 | 111 | class AllGroupDetail(DetailWindow): 112 | def __init__(self, *args, **kwargs): 113 | super().__init__(*args, **kwargs) 114 | 115 | self.headerbar.set_title('All Lights') 116 | 117 | # Remove ui elements that indicate modifiability 118 | # This is definitely on the hacky side, but it'll do for now 119 | self.headerbar.remove(self.headerbar.get_children()[1]) 120 | self.content.remove(self.name_entry_container) 121 | 122 | 123 | class SelectableLightList(Gtk.Frame): 124 | def __init__(self, lights, initial_selection, *args, **kwargs): 125 | super().__init__( 126 | *args, 127 | can_focus=False, 128 | shadow_type=Gtk.ShadowType.NONE, 129 | visible=True, 130 | **kwargs 131 | ) 132 | 133 | builder = Gtk.Builder() 134 | builder.add_from_resource(get_resource_path('ui/group-detail.ui')) 135 | builder.connect_signals(self) 136 | 137 | content = builder.get_object('content-wrapper') 138 | lights_list = builder.get_object('light-list') 139 | 140 | self.lights = lights 141 | self._selected_lights = initial_selection 142 | 143 | for light in self.lights: 144 | row = Gtk.ListBoxRow( 145 | activatable=False, 146 | can_focus=False, 147 | visible=True 148 | ) 149 | 150 | box = Gtk.Box( 151 | can_focus=False, 152 | visible=True, 153 | margin_start=12, 154 | margin_end=6, 155 | margin_top=8, 156 | margin_bottom=8 157 | ) 158 | 159 | check_box = Gtk.CheckButton( 160 | active=light.light_id in self.selected_lights, 161 | border_width=6, 162 | can_focus=True, 163 | draw_indicator=True, 164 | label=light.name, 165 | receives_default=False, 166 | visible=True 167 | ) 168 | 169 | check_box.connect('toggled', self._on_light_toggle, light) 170 | 171 | box.add(check_box) 172 | row.add(box) 173 | lights_list.add(row) 174 | 175 | self.add(content) 176 | 177 | @property 178 | def selected_lights(self): 179 | return self._selected_lights 180 | 181 | def _on_light_toggle(self, check_button, light): 182 | if check_button.get_active(): 183 | self.selected_lights.add(light.light_id) 184 | else: 185 | self.selected_lights.remove(light.light_id) 186 | -------------------------------------------------------------------------------- /luminance/views/groups.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gdk', '3.0') 4 | gi.require_version('Gtk', '3.0') 5 | 6 | from gi.repository import Gdk 7 | from gi.repository import Gtk 8 | 9 | import phue 10 | 11 | from .. import get_resource_path 12 | from .entity import ListBoxRow 13 | from .group import AllGroupDetail 14 | from .group import GroupDetail 15 | from .group import NewGroup 16 | 17 | class Groups(Gtk.Box): 18 | def __init__(self, bridge, *args, **kwargs): 19 | super().__init__(*args, **kwargs) 20 | 21 | self.bridge = bridge 22 | 23 | builder = Gtk.Builder() 24 | builder.add_from_resource(get_resource_path('ui/groups.ui')) 25 | builder.connect_signals(self) 26 | 27 | self.content = builder.get_object('content-wrapper') 28 | self.groups_list = builder.get_object('list') 29 | 30 | self.groups_list.add(ListBoxRow(phue.AllLights(self.bridge))) 31 | for group in self.bridge.groups: 32 | self.groups_list.add(ListBoxRow(group)) 33 | 34 | self.add(self.content) 35 | 36 | def _on_row_activated(self, listbox, row): 37 | if row.model.group_id == 0: 38 | AllGroupDetail( 39 | row.model, 40 | modal=True, 41 | transient_for=self.get_toplevel(), 42 | type_hint=Gdk.WindowTypeHint.DIALOG 43 | ).present() 44 | else: 45 | GroupDetail( 46 | row.model, 47 | modal=True, 48 | transient_for=self.get_toplevel(), 49 | type_hint=Gdk.WindowTypeHint.DIALOG 50 | ).present() 51 | 52 | def _on_new_group_clicked(self, *args): 53 | dialog = NewGroup( 54 | self.bridge, 55 | transient_for=self.get_toplevel() 56 | ) 57 | 58 | response = dialog.run() 59 | 60 | if response == Gtk.ResponseType.APPLY: 61 | self.bridge.create_group(dialog.name, dialog.lights) 62 | 63 | dialog.destroy() 64 | -------------------------------------------------------------------------------- /luminance/views/light.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | import gi 4 | 5 | gi.require_version('Gtk', '3.0') 6 | 7 | from gi.repository import Gtk 8 | 9 | from .entity import EntityDetail 10 | from .util import hsv_to_gdk_rgb 11 | from .. import get_resource_path 12 | 13 | 14 | class LightDetail(EntityDetail): 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | -------------------------------------------------------------------------------- /luminance/views/setup.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | import gi 5 | 6 | gi.require_version('Gdk', '3.0') 7 | gi.require_version('Gtk', '3.0') 8 | 9 | from gi.repository import Gdk 10 | from gi.repository import Gio 11 | from gi.repository import GLib 12 | from gi.repository import Gtk 13 | 14 | import requests 15 | 16 | from .. import get_resource_path 17 | 18 | 19 | class Setup(Gtk.Assistant): 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | 23 | self._bridge = None 24 | self.available_bridges = {} 25 | self.selected_bridge = None 26 | 27 | self.connect('prepare', self._on_prepare) 28 | self.set_position(Gtk.WindowPosition.CENTER) 29 | 30 | geometry = Gdk.Geometry() 31 | geometry.min_height = 450 32 | geometry.min_width = 500 33 | 34 | self.set_geometry_hints(None, geometry, Gdk.WindowHints.MIN_SIZE) 35 | 36 | builder = Gtk.Builder() 37 | builder.add_from_resource(get_resource_path('ui/setup.ui')) 38 | builder.connect_signals(self) 39 | 40 | self.search_page = builder.get_object('search-page') 41 | self.append_page(self.search_page) 42 | self.set_page_title(self.search_page, 'Discover Bridge(s)') 43 | self.set_page_type(self.search_page, Gtk.AssistantPageType.INTRO) 44 | 45 | self.results_list = builder.get_object('results-list') 46 | self.results_page = builder.get_object('results-page') 47 | self.append_page(self.results_page) 48 | self.set_page_title(self.results_page, 'Select Bridge') 49 | self.set_page_type(self.results_page, Gtk.AssistantPageType.CONTENT) 50 | 51 | self.link_button_page = builder.get_object('link-button-page') 52 | self.append_page(self.link_button_page) 53 | self.set_page_title(self.link_button_page, 'Establish Connection') 54 | self.set_page_type(self.link_button_page, Gtk.AssistantPageType.CUSTOM) 55 | 56 | @property 57 | def bridge(self): 58 | return self._bridge 59 | 60 | @bridge.setter 61 | def bridge(self, value): 62 | self._bridge = value 63 | 64 | def _on_prepare(self, assistant, page): 65 | if page == self.search_page: 66 | threading.Thread( 67 | args=(self._on_search_complete,), 68 | daemon=True, 69 | target=self.search 70 | ).start() 71 | elif page == self.link_button_page: 72 | self.link_button_page.set_visible_child_name('press-link-button') 73 | threading.Thread( 74 | args=(self._on_connection_established,), 75 | daemon=True, 76 | target=self.try_bridge_connection 77 | ).start() 78 | 79 | def _on_bridge_selected(self, list_box, row): 80 | self.selected_bridge = self.available_bridges[row]['address'] 81 | 82 | def _on_manual_clicked(self, *args): 83 | self.results_page.set_visible_child_name('manual') 84 | 85 | def _on_manual_address_changed(self, entry_buffer, *args): 86 | self.selected_bridge = entry_buffer.get_text() 87 | 88 | if self.selected_bridge: 89 | self.set_page_complete(self.results_page, True) 90 | else: 91 | self.set_page_complete(self.results_page, False) 92 | 93 | def _on_search_complete(self, results): 94 | if results: 95 | for row in self.available_bridges.keys(): 96 | self.results_list.remove(row) 97 | 98 | self.available_bridges = {} 99 | 100 | def make_bridge_row(bridge): 101 | row = Gtk.ListBoxRow( 102 | can_focus=False, 103 | visible=True 104 | ) 105 | 106 | box = Gtk.Box( 107 | can_focus=False, 108 | margin_start=12, 109 | margin_end=12, 110 | margin_top=8, 111 | margin_bottom=8, 112 | valign=Gtk.Align.CENTER, 113 | visible=True 114 | ) 115 | 116 | label = Gtk.Label( 117 | can_focus=False, 118 | label=bridge['display'], 119 | visible=True, 120 | xalign=0 121 | ) 122 | 123 | box.add(label) 124 | row.add(box) 125 | 126 | return row 127 | 128 | row = make_bridge_row(results[0]) 129 | self.results_list.add(row) 130 | self.available_bridges[row] = results[0] 131 | self.selected_bridge = results[0]['address'] 132 | 133 | for result in results[1:]: 134 | row = make_bridge_row(result) 135 | self.results_list.add(row) 136 | self.available_bridges[row] = result 137 | 138 | self.set_page_complete(self.search_page, True) 139 | self.results_page.set_visible_child_name('results') 140 | self.set_page_complete(self.results_page, True) 141 | else: 142 | self.results_page.set_visible_child_name('no-bridges-found') 143 | 144 | self.next_page() 145 | 146 | return False 147 | 148 | def search(self, cb): 149 | try: 150 | data = requests.get('https://www.meethue.com/api/nupnp').json() 151 | 152 | if not data: 153 | raise ValueError('No bridges registered with Philips') 154 | except Exception: 155 | import urllib.parse 156 | 157 | import netdisco.discovery 158 | 159 | network_discovery = netdisco.discovery.NetworkDiscovery( 160 | limit_discovery=['philips_hue'] 161 | ) 162 | network_discovery.scan() 163 | results = [ 164 | { 165 | 'display': result[0], 166 | 'address': urllib.parse.urlparse(result[1]).hostname 167 | } for result in network_discovery.get_info('philips_hue') 168 | ] 169 | else: 170 | results = [] 171 | 172 | for result in data: 173 | ip = result['internalipaddress'] 174 | res = requests.get('http://{ip}/description.xml'.format(ip=ip)) 175 | 176 | name = [_ for _ in filter( 177 | lambda line: 'friendlyName' in line, 178 | res.text.splitlines() 179 | )] 180 | 181 | assert(len(name) == 1) 182 | 183 | # Yea, this is easier than actually trying to parse the xml... 184 | name = name[0] \ 185 | .replace('', '') \ 186 | .replace('', '') 187 | 188 | results.append({'display': name, 'address': ip}) 189 | finally: 190 | GLib.idle_add(cb, results) 191 | 192 | def _on_connection_established(self): 193 | if self.bridge: 194 | self.emit('apply') 195 | else: 196 | self.link_button_page.set_visible_child_name('unknown-error') 197 | 198 | return False 199 | 200 | def try_bridge_connection(self, cb): 201 | import phue 202 | 203 | while True: 204 | try: 205 | bridge = phue.Bridge(ip=self.selected_bridge) 206 | except phue.PhueRegistrationException: 207 | time.sleep(1) 208 | except Exception: 209 | import traceback 210 | traceback.print_exc() 211 | break 212 | else: 213 | self.bridge = bridge 214 | break 215 | 216 | GLib.idle_add(cb) 217 | -------------------------------------------------------------------------------- /luminance/views/util.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | import gi 4 | 5 | gi.require_version('Gdk', '3.0') 6 | 7 | from gi.repository import Gdk 8 | 9 | 10 | def hsv_to_gdk_rgb(hue, sat, bri): 11 | rgb = colorsys.hsv_to_rgb( 12 | hue / 65535, 13 | sat / 255, 14 | bri / 255 15 | ) 16 | 17 | return Gdk.RGBA(red=rgb[0], green=rgb[1], blue=rgb[2]) 18 | -------------------------------------------------------------------------------- /luminance/views/window.py: -------------------------------------------------------------------------------- 1 | import gi 2 | 3 | gi.require_version('Gdk', '3.0') 4 | gi.require_version('Gtk', '3.0') 5 | 6 | from gi.repository import Gdk 7 | from gi.repository import GdkPixbuf 8 | from gi.repository import Gio 9 | from gi.repository import GLib 10 | from gi.repository import Gtk 11 | 12 | from .. import get_resource_path 13 | from .bridge import Bridge 14 | from .entity import FramedEntityList 15 | from .groups import Groups 16 | 17 | 18 | class Window(Gtk.ApplicationWindow): 19 | def __init__(self, bridge, *args, **kwargs): 20 | super().__init__(*args, **kwargs) 21 | 22 | self.bridge = bridge 23 | 24 | GLib.set_application_name('Luminance') 25 | self.set_position(Gtk.WindowPosition.CENTER) 26 | self.set_icon_name('luminance') 27 | 28 | builder = Gtk.Builder() 29 | builder.add_from_resource(get_resource_path('ui/main.ui')) 30 | builder.connect_signals(self) 31 | 32 | self.header_bar = builder.get_object('headerbar') 33 | self.status_bar = builder.get_object('status-bar') 34 | 35 | self.main_content = builder.get_object('main-content') 36 | self.main_stack = builder.get_object('main-stack') 37 | 38 | self.lights_page = builder.get_object('lights-page') 39 | self.lights_page.add(FramedEntityList(self.bridge.get_light_objects('id').values())) 40 | 41 | self.groups_page = builder.get_object('groups-page') 42 | self.groups_page.add(Groups(self.bridge)) 43 | 44 | self.bridge_page = builder.get_object('bridge-page') 45 | self.bridge_page.add(Bridge(self.bridge)) 46 | 47 | self._on_connection_change() 48 | 49 | geometry = Gdk.Geometry() 50 | geometry.min_height = 450 51 | geometry.min_width = 575 52 | self.set_geometry_hints(None, geometry, Gdk.WindowHints.MIN_SIZE) 53 | self.set_titlebar(self.header_bar) 54 | 55 | self.add(self.main_content) 56 | 57 | def _on_connection_change(self): 58 | self.status_bar.push( 59 | self.status_bar.get_context_id('host-status'), 60 | 'Connected: {host}'.format(host=self.bridge.ip) 61 | ) 62 | 63 | def reload(self, bridge): 64 | print('reload triggered') 65 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigcabrey/luminance/33a274c4254c71cb414e0ac93ee8dd02bc8e508e/screenshot.png --------------------------------------------------------------------------------