{"id":31497,"date":"2024-09-08T10:22:10","date_gmt":"2024-09-08T17:22:10","guid":{"rendered":"https:\/\/www.podfeet.com\/blog\/?p=31497"},"modified":"2024-09-08T10:24:35","modified_gmt":"2024-09-08T17:24:35","slug":"philips-hue-programming-off-the-charts-part-3-of-3","status":"publish","type":"post","link":"https:\/\/www.podfeet.com\/blog\/2024\/09\/philips-hue-programming-off-the-charts-part-3-of-3\/","title":{"rendered":"Philips Hue Programming, Off the Charts (Part 3 of 3)"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" class=\"alignright size-medium wp-image-31498\" src=\"https:\/\/www.podfeet.com\/blog\/wp-content\/uploads\/2024\/07\/Philips-Hue-kit.jpg-300x225.png\" alt=\"A product image of a Philips Hue Starter Kit. It shows a large black box with product imagery on the front. In front of the box are its contents. These include two bulbs, a dimmer knob, and a bridge.\" width=\"300\" height=\"225\" srcset=\"https:\/\/www.podfeet.com\/blog\/wp-content\/uploads\/2024\/07\/Philips-Hue-kit.jpg-300x225.png 300w, https:\/\/www.podfeet.com\/blog\/wp-content\/uploads\/2024\/07\/Philips-Hue-kit.jpg-1024x768.png 1024w, https:\/\/www.podfeet.com\/blog\/wp-content\/uploads\/2024\/07\/Philips-Hue-kit.jpg-768x576.png 768w, https:\/\/www.podfeet.com\/blog\/wp-content\/uploads\/2024\/07\/Philips-Hue-kit.jpg-1536x1152.png 1536w, https:\/\/www.podfeet.com\/blog\/wp-content\/uploads\/2024\/07\/Philips-Hue-kit.jpg-650x488.png 650w, https:\/\/www.podfeet.com\/blog\/wp-content\/uploads\/2024\/07\/Philips-Hue-kit.jpg.png 2048w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><a href=\"https:\/\/www.podfeet.com\/blog\/2024\/08\/philips-hue-programming-evolved-part-2-of-3\/\" rel=\"noopener\" target=\"_blank\">In the second instalment of this series<\/a>, I left you with a couple of useful scripts \u2014 one to fetch the device ID of a light, given a name, and one to perform the conversion from RGB to CIE colour. I developed those two scripts as necessary parts of what comes next.<\/p>\n<p>I have demonstrated the actioning of four different aspects of a light: power, colour temperature, colour, and brightness. In my goal of creating bespoke automation, I wanted simplicity. I didn\u2019t want to use different scripts for different tasks. I didn\u2019t want to have to use \u201cmagic numbers\u201d (like 153 mirek), and I wanted an easy way to specify any combination of change, and across multiple lights in one go.<\/p>\n<p>The result of this\u2026 insanity\u2026 is a single script which addresses all of these aspects. So, how did I get there? Well\u2026 buckle up.<\/p>\n<p>My first requirement was that I didn\u2019t want a script in my home directory with bespoke parameters \u2014 it had to operate like a native command. I achieved this in three ways. First, I named my script simply <code>hue<\/code>. There is no <code>.sh<\/code> suffix. Second, I placed it in a directory that was already in my path. Third, I implemented standard command parameters.<\/p>\n<p>In my case, the directory <code>~\/.local\/bin<\/code> was already present and in my path. This appears to have been created by a third party tool but, from what I have read, it is a recommended location for user scripts. I will leave it as an exercise for the user to worry about where to put the script and how to make that location a permanent part of your path. We have more than enough to cover here already.<\/p>\n<p>For the handling of standard parameters, I have used a third party tool to help me out. I did not consider rolling my own use of <code>getopt<\/code> or <code>getopts<\/code> because both have competing drawbacks. <code>getopt<\/code> is far harder to use but allows long option names. <code>getopts<\/code> is much easier to use but does not have long option names. So, I called on the services of <a href=\"https:\/\/argbash.dev\/\">Argbash<\/a>.<\/p>\n<p>Argbash is a downloadable tool, but I always use the website provided by the developer, which means I don\u2019t need to install anything. I will leave the reader\/listener to peruse the Argbash website but, in a nutshell, you simply provide a description of your parameters in specially-formatted bash comments, then ask Argbash to generate the code to handle them.<\/p>\n<p>I created a very basic script that defined my parameters and had just enough code to prove they worked. I\u2019ve switched to the <code>bash<\/code> shell because that\u2019s what Argbash knows. It\u2019s in the name.<\/p>\n<pre><code class=\"language-bash\">#!\/bin\/bash\n#\n# ARG_OPTIONAL_SINGLE([switch], , [Switch 'on' or 'off'])\n# ARG_OPTIONAL_SINGLE([rgb], , [RGB triplet (0-255)])\n# ARG_OPTIONAL_SINGLE([temp], , [Colour temperature (2000-6500)K])\n# ARG_OPTIONAL_SINGLE([dim], , [Dimming level (1-100)])\n# ARG_POSITIONAL_INF([lights], [Lights to set], 1)\n# ARG_HELP([Allows control of Philips Hue lights via Hue Bridge])\n# ARGBASH_GO\n\n# [ &lt;-- needed because of Argbash\n\nif [ \"$_arg_switch\" != \" \" ]; then\n  echo \"Switch $_arg_switch\"\nfi\nif [ \"$_arg_rgb\" != \" \" ]; then\n  echo \"RGB $_arg_rgb\"\nfi\nif [ \"$_arg_temp\" != \" \" ]; then\n  echo \"temp $_arg_temp\"\nfi\nif [ \"$_arg_dim\" != \" \" ]; then\n  echo \"dim $_arg_dim\"\nfi\necho \"Lights:\"\nfor light in \"${_arg_lights[@]}\"; do\n  echo \"&gt; $light\"\ndone\n\n# ] &lt;-- needed because of Argbash\n\n<\/code><\/pre>\n<p>My parameters are comprised of four optional flags; one each for switching on or off, setting a colour, setting a colour temperature, and setting brightness. Beyond those I allow for an infinite number of light names. The initial script simply prints out the parameters it is given.<\/p>\n<p>I will spare readers the full code generated by Argbash, but suffice to say it worked essentially as I wanted, so I did the rest directly in the generated script without needing to return to Argbash. I\u2019ve left the Argbash lines at the top for later reference should I end up expanding on the script.<\/p>\n<p>With my flags and arguments defined, this is the form of command I was aiming for:<\/p>\n<p><code>hue --rgb 255,127,0 --dim 45 \"Study S1\" \"Study S2\" \"Study S3\"<\/code><\/p>\n<p>Now onto how the script turns this kind of command into action.<\/p>\n<p>First, I added a couple of lines to define the Hue application key and the Bridge IP address. These were mentioned in part 1 of the series. To use my script, you will need to edit in your own values.<\/p>\n<pre><code class=\"language-bash\"># ----- EDIT THESE ---------------------------\nhak=\"Qn74cB7YlKursSzMYyPL4pr5oLWxayBqhKyjFD10\"  \nhba=\"192.168.1.15\"\n# --------------------------------------------\n<\/code><\/pre>\n<p>I defined a base URL variable that contains the IP address and the correct API path for both querying lights and, with the addition of an ID, actioning them.<\/p>\n<pre><code class=\"language-bash\">burl=\"https:\/\/${hba}\/clip\/v2\/resource\/light\"\n<\/code><\/pre>\n<p>Next up are two functions. These are <em>essentially<\/em> the two scripts I produced last time. The only tweaks were to make them work with the variables in the rest of the script.<\/p>\n<pre><code class=\"language-bash\">function get_id() {\n  local filter='.data[] | select(.metadata.name == \"'\"$light\"'\") | .id'\n  local dev_id=$(curl --request GET -ks --header \"hue-application-key: $hak\" \"${burl}\" | jq -r \"$filter\")\n  echo \"$dev_id\"\n}\n                \nfunction get_colour() {\n  r_srgb=\"$(echo \"scale=4; ${rgb[0]} \/ 255\" | bc)\"\n  g_srgb=\"$(echo \"scale=4; ${rgb[1]} \/ 255\" | bc)\"\n  b_srgb=\"$(echo \"scale=4; ${rgb[2]} \/ 255\" | bc)\"  \n          \n  # Linearise RGB\n  if [ 1 -eq \"$(echo \"${r_srgb} &lt; 0.04045\" | bc)\" ]; then\n    r_lin=$( echo \"${r_srgb}\" | awk '{print $1 \/ 12.92};')\n  else\n    r_lin=$( echo \"${r_srgb}\" | awk '{print (($1 + 0.055) \/ 1.055) ^ 2.4};' )\n  fi                              \n\n  if [ 1 -eq \"$(echo \"${g_srgb} &lt; 0.04045\" | bc)\" ]; then\n    g_lin=$( echo \"${g_srgb}\" | awk '{print $1 \/ 12.92};')\n  else                          \n    g_lin=$( echo \"${g_srgb}\" | awk '{print (($1 + 0.055) \/ 1.055) ^ 2.4};' )\n  fi                    \n\n  if [ 1 -eq \"$(echo \"${b_srgb} &lt; 0.04045\" | bc)\" ]; then\n    b_lin=$( echo \"${b_srgb}\" | awk '{print $1 \/ 12.92};')\n  else\n    b_lin=$( echo \"${b_srgb}\" | awk '{print (($1 + 0.055) \/ 1.055) ^ 2.4};' )\n  fi\n\n  # Calculate XYZ\n  x_cie=$( echo \"${r_lin} ${g_lin} ${b_lin}\" | awk '{print $1 * 0.4124 + $2 * 0.3576 + $3 * 0.1805};')\n  y_cie=$( echo \"${r_lin} ${g_lin} ${b_lin}\" | awk '{print $1 * 0.2126 + $2 * 0.7152 + $3 * 0.0722};')\n  z_cie=$( echo \"${r_lin} ${g_lin} ${b_lin}\" | awk '{print $1 * 0.0193 + $2 * 0.1192 + $3 * 0.9505};')\n  \n  json=\"\\\"color\\\":{\\\"xy\\\":{\\\"x\\\":$x_cie,\\\"y\\\":$y_cie}}\"\n  echo $json\n} \n<\/code><\/pre>\n<p>Note that each of these functions simply echoes out the result. When we call them, we can use the same <code>$(...)<\/code> construct to capture the value.<\/p>\n<p>Now we move onto the first functional part of the new script. Because fetching light IDs takes a not-insignificant amount of time, especially given I am allowing for an arbitrary number of them, I decided it was worth spending the time up front to gather all of the needed IDs. This is done with the <code>get_id<\/code>function defined above. Also in this process, I am silently discarding any lights that could not be found. The result is an array of light IDs for use later.<\/p>\n<p>It is worth noting at this point that shells were not designed to be fast. If you want instantaneous action across a significant number of lights, you\u2019re better off using a different language.<\/p>\n<pre><code class=\"language-bash\"># Fetch valid light IDs\nids=()\nfor light in \"${_arg_lights[@]}\"; do\n  light_id=$(get_id)            \n  if [ \"$light_id\" != \"\" ]; then\n    ids+=(\"$light_id\")\n  fi\ndone  \n<\/code><\/pre>\n<p>The next phase of the script is validation of the provided values. There may be edge cases, but I think these capture and complain about any fundamentally incorrect values.<\/p>\n<p>First up, I check whether I have at least one valid light ID.<\/p>\n<pre><code class=\"language-bash\"># No valid lights\nif [ \"${#ids[@]}\" = \"0\" ]; then\n  echo \"No valid lights given.\"\n  exit 1\nfi\n<\/code><\/pre>\n<p>If I don\u2019t, the script prints a suitable message and exits right then. All of the following validations follow this basic pattern.<\/p>\n<p>The next validation is the next most simple. If the <code>--switch<\/code> flag is specified, its value must be <code>on<\/code> or <code>off<\/code>. This is all fairly basic <code>bash<\/code> logic.<\/p>\n<pre><code class=\"language-bash\"># Switch must be on or off\nif [ \"$_arg_switch\" != \"\" ]; then\n  if [ \"$_arg_switch\" != \"on\" ] &amp;&amp; [ \"$_arg_switch\" != \"off\" ]; then\n    echo \"--switch must be 'on' or 'off'.\"\n    exit 1\n  fi\nfi\n<\/code><\/pre>\n<p>Validating the <code>--rgb<\/code> flag is a bit more involved. The user must supply 3 values separated by commas, they must be valid integers, and they must be in the range from 0 to 255.<\/p>\n<pre><code class=\"language-bash\"># Validate RGB triplet\nif [ \"$_arg_rgb\" != \"\" ]; then\n  IFS=',' read -ra rgb &lt;&lt;&lt; \"$_arg_rgb\"\n  if [ \"${#rgb[@]}\" != \"3\" ]; then\n    echo \"--rgb must supply 3 comma-separated values\"\n    exit 1\n  fi\n  for c in \"${rgb[@]}\"; do\n    if ! [[ $c =~ ^[0-9]+$ ]]; then\n      echo \"--rgb must supply 3 comma separated numbers, e.g. --rgb 100,255,0\"\n      exit 1\n    fi\n    if [[ $c -lt 0 || $c -gt 255 ]]; then\n      echo \"--rgb must supply 3 comma separated values in the range 0-255\"\n      exit 1\n    fi\n  done\nfi\n<\/code><\/pre>\n<p>There are several more interesting <code>bash<\/code> constructs in that check. First, splitting up the comma-separated triplet into an array, which involves setting the separator and <code>read<\/code>ing the values into the array. If the array has any number of elements other than three, then I complain.<\/p>\n<p>Once I know I have three values, I check they contain only digits and again complain if not. This uses the very powerful <code>bash<\/code> regular expression comparison, though in a fairly simple way.<\/p>\n<p>Finally, I now know I have three integers, so I check to see they are in the range 0-255.<\/p>\n<p>Up next, and a little simpler, is the <code>--temp<\/code> flag for colour temperature.<\/p>\n<pre><code class=\"language-bash\"># Temp must be in the range 2000-6500\nif [ \"$_arg_temp\" != \"\" ]; then\n  if ! [[ $_arg_temp =~ ^[0-9]+$ ]]; then\n    echo \"--temp must be an integer in the range 2000-6500\"\n    exit 1\n  fi\n  if [[ $_arg_temp -lt 2000 ]] || [[ $_arg_temp -gt 6500 ]]; then\n    echo \"--temp must be in the range 2000-6500\"\n    exit 1\n  fi\nfi  \n<\/code><\/pre>\n<p>Once again, a check is made for only digits and when that is satisfied, I check the number is in the range 2000-6500.<\/p>\n<p>The final check is the <code>--dim<\/code> flag for brightness. This needs to be an integer from 0-100. The checks are nearly identical to the colour temperature ones, just a different range check.<\/p>\n<pre><code class=\"language-bash\"># Dimming must be in the range 0-100\nif [ \"$_arg_dim\" != \"\" ]; then\n  if ! [[ $_arg_dim =~ ^[0-9]+$ ]]; then\n    echo \"-- dim must be an interger in the range 0-100\"\n    exit 1\n  fi\n  if [[ $_arg_dim -lt 0 ]] || [[ $_arg_dim -gt 100 ]]; then\n    echo \"--dim must be in the range 0-100\"\n    exit 1\n  fi\nfi\n<\/code><\/pre>\n<p>Once I get to this point in the script, whatever values I have been given are in good shape, so the next phase is to build up the JSON string that will write the actions to the lights.<\/p>\n<p>For each of the flags provided, these sections will add to the <code>json<\/code> variable using straightforward string concatenation with some conditionals thrown in. The <code>--switch<\/code> flag section is the first.<\/p>\n<pre><code class=\"language-bash\"># Build up JSON actions\njson=\"\"\n\n# Switch on or off\nif [ \"$_arg_switch\" != \"\" ]; then\n  json=\"${json}\\\"on\\\":{\\\"on\\\":\"\n  if [ \"$_arg_switch\" = \"on\" ]; then\n    json=\"${json}true\"\n  else  \n    json=\"${json}false\" \n  fi\n  json=\"${json}}\"\nfi\n<\/code><\/pre>\n<p>After this, each section also has an extra conditional to insert a comma if there is prior content.<\/p>\n<pre><code class=\"language-bash\"># RGB\nif [ \"$_arg_rgb\" != \"\" ]; then  \n  if [ \"$json\" != \"\" ]; then\n    json=\"${json},\"\n  fi\n  json=\"${json}$(get_colour)\"\nfi\n  \n# Temperature\nif [ \"$_arg_temp\" != \"\" ]; then\n  if [ \"$json\" != \"\" ]; then\n    json=\"${json},\"\n  fi\n  mirek=$(echo \"scale=0; 1000000 \/ ${_arg_temp}\" | bc)\n  json=\"${json}\\\"color_temperature\\\":{\\\"mirek\\\":${mirek}}\"\nfi\n      \n# Dimming\nif [ \"$_arg_dim\" != \"\" ]; then\n  if [ \"$json\" != \"\" ]; then\n    json=\"${json},\"\n  fi  \n  json=\"${json}\\\"dimming\\\":{\\\"brightness\\\":${_arg_dim}}\"\nfi  \n<\/code><\/pre>\n<p>The <code>--rgb<\/code> flag processing calls the <code>get_colour<\/code> function to get the complete JSON object. The <code>--temp<\/code> flag processing does the simple mirek calculation using <code>bc<\/code>.<\/p>\n<p>Finally, the series of JSON objects are wrapped in an outer object before looping through the previously-collected IDs and applying the actions with the curl command.<\/p>\n<pre><code class=\"language-bash\"># Wrap the json in an object\njson=\"{${json}}\"\n    \n# Finally, iterate over the lights and action each\nfor id in \"${ids[@]}\"; do\n  url=\"${burl}\/${id}\"\n  curl --request PUT -ks --data \"${json}\" --header \"hue-application-key: $hak\" \"${url}\" &gt; \/dev\/null\ndone\n<\/code><\/pre>\n<p>That\u2019s it! The complete script is 311 lines long. Without blank lines and comments, it is 236 lines, and ignoring the Argbash bits, only 123 lines of code that I have written myself.<\/p>\n<p>In use, the script can seem a little slow, but in my case, I have 6 lights, and 1 switch connected to my bridge and it is able to set 3 of the lights\u2019 colour temperature and brightness in 1 second. Setting a colour takes more work in the conversion of RGB to CIE and sets the same three lights, including brightness, in around 1.6 seconds. In both cases, I can see the changes ripple across the three bulbs.<\/p>\n<p>The speed could be improved either with a more efficient language, or by looking at storing IDs rather than looking up each one every time. Or you could look into light grouping. There is plenty of scope to improve on my effort.<\/p>\n<p>For my own purposes, I will be using Better Touch Tool to script my Elgato Stream Deck buttons to perform various actions on my lights. The possibilities are many!<\/p>\n<p>You can find the full script <a href=\"https:\/\/gist.github.com\/zkarj735\/6bfdb0a6311d17c3a8b86e9a31df2e5d\" target=\"_blank\" rel=\"noopener\">on my GitHub account<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In the second instalment of this series, I left you with a couple of useful scripts \u2014 one to fetch the device ID of a light, given a name, and one to perform the conversion from RGB to CIE colour. I developed those two scripts as necessary parts of what comes next. I have demonstrated [&hellip;]<\/p>\n","protected":false},"author":29,"featured_media":31498,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[147],"tags":[1356,1136,176,229],"class_list":["post-31497","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-posts","tag-controlled-lighting","tag-home-automation","tag-programming","tag-terminal"],"jetpack_featured_media_url":"https:\/\/www.podfeet.com\/blog\/wp-content\/uploads\/2024\/07\/Philips-Hue-kit.jpg.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/posts\/31497","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/users\/29"}],"replies":[{"embeddable":true,"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/comments?post=31497"}],"version-history":[{"count":5,"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/posts\/31497\/revisions"}],"predecessor-version":[{"id":31855,"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/posts\/31497\/revisions\/31855"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/media\/31498"}],"wp:attachment":[{"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/media?parent=31497"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/categories?post=31497"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.podfeet.com\/blog\/wp-json\/wp\/v2\/tags?post=31497"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}