[
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# SmartThings\nhttps://github.com/codersaur/SmartThings\n\nCopyright (c) 2017 [David Lomas](https://github.com/codersaur)\n\n## Overview\n\nThis repository contains device handlers and SmartApps for use with Samsung's [SmartThings](http://www.smartthings.com) home automation platform.\n\n## SmartApps\n\n#### [Evohome (Connect) - BETA](https://github.com/codersaur/SmartThings/tree/master/smartapps/evohome-connect):\n - This SmartApp connects your Honeywell Evohome System to SmartThings.\n - Note, the Evohome Heating Zone device handler (below) must also be installed.\n\n#### [InfluxDB Logger](https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger):\n - This SmartApp logs SmartThings device attributes to an [InfluxDB](https://influxdata.com/) database.\n\n### SmartApp Installation Procedure\n\n#### Part One: Install the code using the SmartThings IDE\n\n1. Within the SmartThings IDE, click '*My SmartApps*', then '*+ New SmartApp*'. \n2. Select the '*From Code*' tab and paste in the contents of the relevant groovy file.\n3. Click '*Create*', and then '*Publish*' *(For Me)*.\n\n#### Part Two: Create a SmartApp instance\n\n1. Using the SmartThings app on your phone, navigate to the '*Marketplace*'.\n2. Select '*SmartApps*', then browse to '*My Apps*' at the bottom of the list.\n3. Select the new SmartApp, complete the configuration options and press '*Done*'.\n\n**Note:** Some SmartApps may support multiple instances, whereas others may only allow one instance.\n\n## Device Handlers\n\n#### [Aeon Home Energy Meter (GEN2 - UK - 1 Clamp)](https://github.com/codersaur/SmartThings/tree/master/devices/aeon-home-energy-meter):\n - This device handler is written specifically for the Aeon Home Energy Meter Gen2 UK version, with a single clamp.\n - It supports live reporting of energy, power, current, and voltage, as well as energy and cost statistics over multiple pre-defined periods.\n\n#### [Evohome Heating Zone - BETA](https://github.com/codersaur/SmartThings/tree/master/devices/evohome):\n - This device handler is required for the Evohome (Connect) SmartApp.\n\n#### [Fibaro Dimmer 2 (FGD-212)](https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2):\n - An advanced device handler for the Fibaro Dimmer 2 (FGD-212) Z-Wave Dimmer, with support for full parameter synchronisation, multi-channel device associations, protection modes, fault reporting, and advanced logging options.\n - The _Nightmode_ function forces the dimmer to switch on at a specific level (e.g. low-level during the night). It can be enabled/disabled manually using the _Nightmode_ tile, or scheduled from the device's settings.  \n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-on.png\" width=\"200\">\n\n#### [Fibaro Flood Sensor (FGFS-101)](https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-flood-sensor):\n - An advanced SmartThings device handler for the Fibaro Flood Sensor (FGFS-101) (EU), with support for full parameter synchronisation, multi-channel device associations, and advanced logging options.  \n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-flood-sensor/screenshots/ffs-ss-tiles-wet.png\" width=\"200\">\n\n#### [Fibaro RGBW Controller (FGRGBWM-441)](https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-rgbw-controller):\n - This device handler is written specifically for the Fibaro RGBW Controller (FGRGBWM-441).\n - It extends the native SmartThings device handler to support editing the device's parameters from the SmartThings GUI, and to support the use of one or more of the controller's channels in IN/OUT mode (i.e. analog sensor inputs).  \n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_rgbw.png\" width=\"200\">\n \n#### [GreenWave PowerNode (Single) (NS210-G-EN)](https://github.com/codersaur/SmartThings/tree/master/devices/greenwave-powernode-single):\n  - An advanced SmartThings device handler for the GreenWave PowerNode (Single socket) Z-Wave power outlet, with support for power and energy reporting, the _Room Colour Wheel_, local and RF protection modes, an _Auto-off Timer_, full parameter synchronisation, and advanced logging options.  \n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-main.png\" width=\"200\">\n\n#### [Philio Dual Relay (PAN04)](https://github.com/codersaur/SmartThings/tree/master/devices/philio-dual-relay):\n - This device handler is written specifically for the Philio Dual Relay (PAN04), when used as a single switch/relay only.\n - It supports live reporting of energy, power, current, voltage, and power factor,  as well as energy and cost statistics over multiple pre-defined periods.\n \n#### [TKB Metering Switch (TZ88E-GEN5)](https://github.com/codersaur/SmartThings/tree/master/devices/tkb-metering-switch):\n - This device handler is written specifically for the TKB Metering Switch (TZ88E-GEN5).\n - It supports live reporting of energy, power, current, voltage, and power factor,  as well as energy and cost statistics over multiple pre-defined periods.\n \n#### [Z-Wave Tweaker](https://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker):\n - A SmartThings device handler to assist with interrogating and tweaking Z-Wave devices. Useful for end-users and SmartThings developers.  \n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-tiles-main.png\" width=\"200\">\n\n### Device Handler Installation Procedure\n\n#### Part One: Install the device handler code using the SmartThings IDE\n\n1. Within the SmartThings IDE, click on '*My Device Handlers*'.\n2. Click the '*+ Create New Device Handler*' button. \n3. Select the '*From Code*' tab and paste in the contents of the relevant groovy file.\n4. Click '*Create*'.\n5. Click '*Publish*' *(For Me)*.\n\n#### Part Two: Connect your device to SmartThings\n\nIf your device is already connected to SmartThings, you can skip straight to part three, however if your physical device is not yet connected to SmartThings, you will need to follow [these instructions to _Add a Thing_](https://support.smartthings.com/hc/en-gb/articles/205956950-How-to-connect-and-configure-new-devices).\n\nDuring the joining process SmartThings will select an appropriate device handler, if the correct device handler (installed in part one) is selected, you can skip to part four, otherwise you will need to change the handler as described in part three.\n\n#### Part Three: Update existing device types\n\nWhen you add new devices, SmartThings will automatically select the device handler with the closest-matching *fingerprint*. However, this process is not perfect and it often fails to select the desired device handler. You may also have pre-existing devices you want to switch to new device handler. In these cases, you need to change the device type of each device instance from the IDE.\n\n1. Within the SmartThings IDE, click on '*My Devices*'.\n2. Click on the appropriate device to bring up its properties.\n3. Click the '*Edit*' button at the bottom.\n4. Change the '*Type*' using the drop-down box (custom devices will be near the bottom of the list).\n5. Hit the '*Update*' button at the bottom.\n\n#### Part Four: Update existing device settings\n\nIf you have changed the type of an existing device, it is very important to update the device's settings to ensure the device instance is fully initialised and ready for use with the new device handler.\n\n1. Within the SmartThings IDE, click on the '*Live Logging*' tab to monitor an messages generated by the following steps. \n2. In the SmartThings app on your phone, navigate to the device (you should find the GUI has changed to reflect the new tiles configuration).\n3. Press the gear icon to edit the device's settings.\n4. Review each setting to ensure it has a suitable value, then press '*Done*'.\n5. Back in the SmartThings IDE, review any messages from the device in the '*Live Logging*' screen. \n \n**Note:** Android users may encounter some errors in the SmartThings app after a device type has been changed. This can usually be resolved by completely closing the SmartThings app and restarting it.\n\n## License\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\nin compliance with the License. You may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed\non an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\nfor the specific language governing permissions and limitations under the License.\n"
  },
  {
    "path": "devices/aeon-home-energy-meter/aeon-home-energy-meter.groovy",
    "content": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: Aeon Home Energy Meter (GEN2 - UK - 1 Clamp)\n *\n *  Author: David Lomas (codersaur)\n *\n *  Date: 2017-03-02\n *\n *  Version: 1.03\n *\n *  Description:\n *   - This device handler is written specifically for the Aeon Home Energy Meter Gen2 UK version, with a single clamp.\n *   - Supports live reporting of energy, power, current, and voltage. Press the 'Now' tile to refresh.\n *      (voltage tile is not shown by default, but you can enable it below).\n *   - Supports reporting of energy usage and cost over an ad hoc period, based on the 'energy' figure reported by \n *     the device. Press the 'Since...' tile to reset.\n *   - Supports additional reporting of energy usage and cost over multiple pre-defined periods:\n *       'Today', 'Last 24 Hours', 'Last 7 Days', 'This Month', 'This Year', and 'Lifetime'\n *     These can be cycled through by pressing the 'statsMode' tile. \n *   - There's a tile that will reset all Energy Stats periods, but it's hidden by default.\n *   - Key device parameters can be set from the device settings. Refer to the Aeon HEMv2 instruction \n *     manual for full details.\n *   - If you are re-using this device, please use your own hosting for the icons.\n *\n *  Version History:\n *\n *   2017-03-02: v1.03:\n *    - Fixed tile formatting for Android.\n *    - Limited power attribute to one decimal place.\n *\n *   2016-02-27: v1.02\n *    - Added \"Voltage Measurement\" capability to metadata (although not currently suppoted by hub).\n *    \n *   2016-02-15: v1.01\n *    - Added reporting of energy usage and cost over multiple pre-defined periods.\n *    - Added ConfigurationReport event parser (useful for debuging).\n *    - Added input preferences for Parameter 2, 4, 8.\n *    - Improved input preference descriptions and ranges.\n *    - Added background colours for mainPower and multi1 tiles. \n *    - Added Instantaneous £/day figure as a secondary info on multi1.\n *\n *   2016-02-05: v1.0 - Initial Version for HEMv2 UK 1 Clamp.\n *    - Added support for voltage (V) and current (A).\n *    - Added fingerprint for HEMv2.\n *    - Added Refresh and Polling capabilities.\n *    - Added input preferences for reporting intervals.\n *    - Added calculation of total cost, based on CostPerKWh setting.\n * \n *  To Do:\n *   - Capture out-of-band energy reset.\n *   - Option to specify a '£/day' fixed charge, which is added to all energy cost figures.\n *   - If the use of 'enum' inputs with \"multiple: true\" is ever fixed by ST, then implement input\n *     preferences to specify Reporting Group Content Flags (Parameters 101-103).\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n */\nmetadata {\n\tdefinition (\n\t\tname: \"Aeon Home Energy Meter (GEN2 - UK - 1 Clamp)\", \n\t\tnamespace: \"codersaur\", \n\t\tauthor: \"David Lomas\"\n\t) \n\t{\n\t\tcapability \"Power Meter\"\n\t\tcapability \"Energy Meter\"\n\t\tcapability \"Voltage Measurement\"\n\t\tcapability \"Polling\"\n\t\tcapability \"Refresh\"\n        capability \"Configuration\"\n\t\tcapability \"Sensor\"\n        \n\t\tcommand \"reset\"\n        command \"resetAllStats\"\n\t\tcommand \"poll\"\n        command \"refresh\"\n        command \"configure\"\n        command \"updated\"\n        command \"cycleStats\"\n\t\tcommand \"test\"\n  \n\t\t// Standard (Capability) Attributes:\n\t\tattribute \"power\", \"number\"\n        attribute \"energy\", \"number\" // Energy (kWh) as reported by device (ad hoc period).\n\n       // Custom Attributes:\n        attribute \"current\", \"number\"\n        attribute \"voltage\", \"number\"\n        //attribute \"powerFactor\", \"number\" - Not supported.\n\t\tattribute \"powerCost\", \"number\"  // Instantaneous Cost of Power (£/day)\n\t\tattribute \"lastReset\", \"string\" // Time that ad hoc reporting was reset.\n\t\tattribute \"statsMode\", \"string\"\n\t\tattribute \"costOfEnergy\", \"number\" \n\t\tattribute \"energyToday\", \"number\"\n\t\tattribute \"costOfEnergyToday\", \"number\"\n\t\tattribute \"energy24Hours\", \"number\"\n\t\tattribute \"costOfEnergy24Hours\", \"number\"\n\t\tattribute \"energy7Days\", \"number\"\n\t\tattribute \"costOfEnergy7Days\", \"number\"\n\t\tattribute \"energyMonth\", \"number\"\n\t\tattribute \"costOfEnergyMonth\", \"number\"\n\t\tattribute \"energyYear\", \"number\"\n\t\tattribute \"costOfEnergyYear\", \"number\"\n\t\tattribute \"energyLifetime\", \"number\"\n\t\tattribute \"costOfEnergyLifetime\", \"number\"\n\n        // Display Attributes:\n        // These are only required because the UI lacks number formatting and strips leading zeros.\n        attribute \"dispPower\", \"string\"\n        attribute \"dispPowerCost\", \"string\"\n        attribute \"dispCurrent\", \"string\"\n        attribute \"dispVoltage\", \"string\"\n        //attribute \"dispPowerFactor\", \"string\" - Not supported.\n        attribute \"dispEnergy\", \"string\"\n        attribute \"dispCostOfEnergy\", \"string\"\n        attribute \"dispEnergyPeriod\", \"string\"\n        attribute \"dispCostOfEnergyPeriod\", \"string\"\n\t\t\n\t\t// Fingerprints:\n\t\tfingerprint deviceId: \"0x3101\", inClusters: \"0x70 0x32 0x60 0x85 0x56 0x72 0x86\"\n\t}\n\n\t// Tile definitions:\n\ttiles(scale: 2) {\n\t\n\t\t// Multi Tile:\n\t\tmultiAttributeTile(name:\"multi1\", type: \"generic\", width: 6, height: 4) {\n\t\t\ttileAttribute (\"device.power\", key: \"PRIMARY_CONTROL\") {\n\t\t\t\tattributeState \"default\", label:'${currentValue} W', backgroundColors: [\n\t\t\t\t\t[value: 0, color: \"#00cc33\"],\n\t\t\t\t\t[value: 250, color: \"#66cc33\"],\n\t\t\t\t\t[value: 500, color: \"#cccc33\"],\n\t\t\t\t\t[value: 750, color: \"#ffcc33\"],\n\t\t\t\t\t[value: 1000, color: \"#ff9933\"], \n\t\t\t\t\t[value: 1500, color: \"#ff6633\"], \n\t\t\t\t\t[value: 2000, color: \"#ff3333\"]\n\t\t\t\t]\t\t\n\t\t\t}\n\t\t\ttileAttribute (\"device.dispPowerCost\", key: \"SECONDARY_CONTROL\") {\n\t\t\t\tattributeState \"default\", label:'(${currentValue})'\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Instantaneous Values:\n\t\tvalueTile(\"instMode\", \"device.dispPower\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Now:', action:\"refresh.refresh\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_refresh.png\"\n\t\t}\n\t\tvalueTile(\"power\", \"device.dispPower\", decoration: \"flat\", width: 2, height: 1, canChangeIcon: true) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"current\", \"device.dispCurrent\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"voltage\", \"device.dispVoltage\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t//valueTile(\"powerFactor\", \"device.dispPowerFactor\", decoration: \"flat\", width: 2, height: 1) {\n\t\t//\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t//}\n\t\t\n        // Ad Hoc Energy Stats:\n\t\tvalueTile(\"lastReset\", \"device.lastReset\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Since:  ${currentValue}', action:\"reset\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_stopwatch_reset.png\"\n\t\t}\n\t\tvalueTile(\"energy\", \"device.dispEnergy\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costOfEnergy\", \"device.dispCostOfEnergy\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t\n\t\t// Energy Stats:\n        // Needs to be a standardTile to be able to change icon for each state.\n\t\tvalueTile(\"statsMode\", \"device.statsMode\", decoration: \"flat\", canChangeIcon: true, canChangeBackground: true, width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"Today\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"Last 24 Hours\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"Last 7 Days\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"This Month\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"This Year\", label:\"${currentValue}:\", action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"Lifetime\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t}\n\t\tvalueTile(\"energyPeriod\", \"device.dispEnergyPeriod\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costOfEnergyPeriod\", \"device.dispCostOfEnergyPeriod\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costPerKWH\", \"device.costPerKWH\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Unit Cost: ${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t\n\t\t// Action Buttons:\n\t\tstandardTile(\"refresh\", \"device.power\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'', action:\"refresh.refresh\", icon:\"st.secondary.refresh\"\n\t\t}\n\t\tstandardTile(\"resetAllStats\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'RESET ALL STATS!', action:\"resetAllStats\"\n\t\t}\n\t\tstandardTile(\"configure\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'', action:\"configuration.configure\", icon:\"st.secondary.configure\"\n\t\t}\n\t\tstandardTile(\"test\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'Test', action:\"test\"\n\t\t}\n\t\t\n\t\t// Tile layouts:\n\t\tmain ([\"multi1\"])\n\t\tdetails([\n\t\t\t// Multi Tile:\n\t\t\t\"multi1\",\n\t\t\t// Instantaneous Values:\n\t\t\t\"instMode\",\"power\", \"current\", //\"voltage\" ,// \"powerFactor\",\n\t\t\t// Ad Hoc Stats:\n\t\t\t\"lastReset\", \"energy\", \"costOfEnergy\",\n\t\t\t// Energy Stats:\n\t\t\t\"statsMode\", \"energyPeriod\", \"costOfEnergyPeriod\"//, //\"costPerKWH\",\n\t\t\t// Action Buttons:\n\t\t\t// \"refresh\",\"resetAllStats\",\"configure\",\"test\"\n\t\t])\n\t}\n\n\t// Preferences:\n\tpreferences {\n    \tsection {\n\t\t\t// Debug Mode:\n\t\t\tinput \"configDebugMode\", \"boolean\", title: \"Enable debug logging?\", defaultValue: false, displayDuringSetup: false\n\t\t\tinput \"configCostPerKWH\", \"string\", title: \"Energy Cost (£/kWh)\", defaultValue: \"0.1316\", required: true, displayDuringSetup: false\n    \t}\n\t\t\n\t\tsection {\n\t\t\t// Native Device Parameters:\n\t\t\t\n\t\t\tinput \"configEnergyDetectionMode\", \"enum\", title: \"Energy Detection Mode:\", options: [\"Wattage, absolute kWh\",\"+/-Wattage, algebraic sum kWh\",\"+/-Wattage, +ive kWh (consuming electricity)\",\"+/-Wattage, -iv kWh (generating electricity)\"], defaultValue: \"Wattage, absolute kWh\", required: true, displayDuringSetup: false\n\t\t\tinput \"configSelectiveReporting\", \"boolean\", title: \"Enable Selective Reporting?\", defaultValue: false, required: true, displayDuringSetup: false\n\t\t\t\n\t\t\t// Parameter 4: \"Power Change Threshold for Auto-Report - Whole HEM (W)\"\n\t\t\tinput \"configPowerThresholdAbs_HEM\", \"number\", title: \"Auto-report Power Threshold (W):\", description: \"Report power when value changes by... W\", defaultValue: 50, range: \"0..60000\", displayDuringSetup: false\n\t\t\t\n\t\t\t// Parameters 5-7 are not needed for single-clamp version.\n\t\t\t\n\t\t\t// Parameter 8: \"Power Percentage Change Threshold for Auto-Report - Whole HEM (%)\"\n\t\t\tinput \"configPowerThresholdPercent_HEM\", \"number\", title: \"Auto-report Power Threshold (%):\", description: \"Report power when value changes by...%\", defaultValue: 10, range: \"0..100\", displayDuringSetup: false\n\t\t\t\n\t\t\t// Parameters 9-11 are not needed for single-clamp version.\n\t\t\t\n\t\t\t// Parameters 101-103 are hard-coded. Will add input preferences if multi-select enum input behaviour is fixed by ST. Currently buggy.\n\t\t\t//  Reporting Group 1 = Power and Current.\n\t\t\t//  Reporting Group 2 = Energy\n\t\t\t//  Reporting Group 3 = Voltage\n\t\t\t\n\t\t\t// Parameter 111: Reporting Group 1 - Report Interval (s):\n\t\t\tinput \"configReportGroup1Interval\", \"number\", title: \"Power/Current Reporting Interval (s):\", defaultValue: 60, range: \"0..2147483647\", displayDuringSetup: false\n\t\t\t\n\t\t\t// Parameter 112: Reporting Group 2 - Report Interval (s):\n\t\t\tinput \"configReportGroup2Interval\", \"number\", title: \"Energy Reporting Interval (s):\", defaultValue: 600, range: \"0..2147483647\", displayDuringSetup: false\n\t\t\t\n\t\t\t// Parameter 113: Reporting Group 3 - Report Interval (s):\n\t\t\tinput \"configReportGroup3Interval\", \"number\", title: \"Voltage Reporting Interval (s):\", defaultValue: 600, range: \"0..2147483647\", displayDuringSetup: false\n\t\t}\n\t}\n\n\t\n\t// simulator metadata\n\tsimulator {\n\t\tfor (int i = 0; i <= 10000; i += 1000) {\n\t\t\tstatus \"power  ${i} W\": new physicalgraph.zwave.Zwave().meterV1.meterReport(\n\t\t\t\tscaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage()\n\t\t}\n\t\tfor (int i = 0; i <= 100; i += 10) {\n\t\t\tstatus \"energy  ${i} kWh\": new physicalgraph.zwave.Zwave().meterV1.meterReport(\n\t\t\t\tscaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage()\n\t\t}\n\t}\n\n}\n\n/**********************************************************************\n *  Z-wave Event Handlers.\n **********************************************************************/\n\n/**\n *  parse - Called when messages from a device are received by the hub.\n *\n *  The parse method is responsible for interpreting those messages and returning Event definitions.\n *\n *  String \t\tdescription \t\t- The message from the device.\n **/\ndef parse(String description) {\n\t//if (state.debug) log.debug \"$device.displayName Parsing raw command: \" + description\n    \n    def result = null\n    \n\t// zwave.parse(): \n    // The second parameter specifies which command version to return for each command type:\n    // Aeon Home Energy Meter Gen2 supports:\n    //  COMMAND_CLASS_METER_V3 [0x32: 3]\n    //  COMMAND_CLASS_CONFIGURATION [0x70: 1]\n    //  COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 [0x72: 2]\n    //  COMMAND_CLASS_MULTI_CHANNEL V3 [????] - Not needed for single clamp device.\n\tdef cmd = zwave.parse(description, [0x32: 3, 0x70: 1, 0x72: 2])\n\tif (cmd) {\n\t\tif (state.debug) log.debug \"$device.displayName zwave.parse() returned: $cmd\"\n\t\tresult = zwaveEvent(cmd)\n\t\tif (state.debug) log.debug \"$device.displayName zwaveEvent() returned: ${result?.inspect()}\"\t\n\t}\n\treturn result\n}\n\n/**\n *  COMMAND_CLASS_METER_V3 (0x32)\n *\n *  Integer\t\t\tdeltaTime\t\t    \t\tTime in seconds since last report\n *  Short\t\t\tmeterType\t\t    \t\tUnknown = 0, Electric = 1, Gas = 2, Water = 3\n *  List<Short>\t\tmeterValue\t\t    \t\tMeter value as an array of bytes\n *  Double\t\t\tscaledMeterValue\t\t\tMeter value as a double\n *  List<Short>\t\tpreviousMeterValue\t\t\tPrevious meter value as an array of bytes\n *  Double\t\t\tscaledPreviousMeterValue    Previous meter value as a double\n *  Short\t\t\tsize\t\t\t\t\t\tThe size of the array for the meterValue and previousMeterValue\n *  Short\t\t\tscale\t\t\t\t\t\tThe scale of the values: \"kWh\"=0, \"kVAh\"=1, \"Watts\"=2, \"pulses\"=3, \"Volts\"=4, \"Amps\"=5, \"Power Factor\"=6, \"Unknown\"=7\n *  Short\t\t\tprecision\t\t\t\t\tThe decimal precision of the values\n *  Short\t\t\trateType\t\t\t\t\t???\n *  Boolean\t\t\tscale2\t\t\t\t\t\t???\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {\n\tif (cmd.scale == 0) {\n    \t// Accumulated Energy (kWh) - Update stats and record energy.\n    \tstate.energy = cmd.scaledMeterValue\n\t\tupdateStats()\n        sendEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" kWh\", displayed: false)\n\t\treturn createEvent(name: \"energy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal), unit: \"kWh\")\n\t}\n\telse if (cmd.scale == 1) {\n    \t// Accumulated Energy (kVAh) - Ignore.\n\t\t//createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kVAh\")\n\t}\n\telse if (cmd.scale == 2) {\n    \t// Instantaneous Power (Watts) - Calculate powerCost and record power:\n\t\tstate.powerCost = cmd.scaledMeterValue * state.costPerKWH * 0.024\n\t\tsendEvent(name: \"powerCost\", value: state.powerCost, unit: \"£/day\")\n        sendEvent(name: \"dispPowerCost\", value: \"£\" + String.format(\"%.2f\",state.powerCost as BigDecimal) + \" per day\", displayed: false)\n        sendEvent(name: \"dispPower\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" W\", displayed: false)\n        return createEvent(name: \"power\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal), unit: \"W\")\n\t}\n\telse if (cmd.scale == 4) {\n    \t// Instantaneous Voltage (Volts)\n\t\tsendEvent(name: \"dispVoltage\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" V\", displayed: false)\n        return createEvent(name: \"voltage\", value: cmd.scaledMeterValue, unit: \"V\")\n\t}\n\telse if (cmd.scale == 5) { \n    \t// Instantaneous Current (Amps)\n\t\tsendEvent(name: \"dispCurrent\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" A\", displayed: false)\n        return createEvent(name: \"current\", value: cmd.scaledMeterValue, unit: \"A\")\n\t}\n\t//else if (cmd.scale == 6) {\n    \t// Instantaneous Power Factor - Not supported.\n\t//\tsendEvent(name: \"dispPowerFactor\", value: \"PF: \" + String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal), displayed: false)\n    //    return createEvent(name: \"powerFactor\", value: cmd.scaledMeterValue, unit: \"PF\")\n\t//}\n}\n\n\n/**\n *  COMMAND_CLASS_CONFIGURATION (0x70)\n *\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {\n\t// Translate the cmd and log the parameter configuration.\n\n\t// Translate value (byte array) back to scaledConfigurationValue (decimal):\n    // This should be done in zwave.parse() but isn't implemented yet.\n    // See: https://community.smartthings.com/t/zwave-configurationv2-configurationreport-dev-question/9771/6\n    // I can't make this work just yet...\n\t//int value = java.nio.ByteBuffer.wrap(cmd.configurationValue as byte[]).getInt()\n    // Instead, a brute force way\n    def scValue = 0\n    if (cmd.size == 1) { scValue = cmd.configurationValue[0]}\n    else if (cmd.size == 2) {  scValue = cmd.configurationValue[1] + (cmd.configurationValue[0] * 0x100) }\n    else if (cmd.size == 3) {  scValue = cmd.configurationValue[2] + (cmd.configurationValue[1] * 0x100) + (cmd.configurationValue[0] * 0x10000) }\n    else if (cmd.size == 4) {  scValue = cmd.configurationValue[3] + (cmd.configurationValue[2] * 0x100) + (cmd.configurationValue[1] * 0x10000) + (cmd.configurationValue[0] * 0x1000000) }\n    \n    // Translate parameterNumber to parameterDescription:\n    def parameterDescription\n    switch (cmd.parameterNumber) {\n        case 2:\n            parameterDescription = \"Energy Detection Mode\"\n            break\n        case 3:\n            parameterDescription = \"Enable Selective Reporting\"\n            break\n        case 4:\n            parameterDescription = \"Power Change Threshold for Auto-Report - Whole HEM (W)\"\n            break\n        case 5:\n            parameterDescription = \"Power Change Threshold for Auto-Report - Clamp 1 (W)\"\n            break\n        case 6:\n            parameterDescription = \"Power Change Threshold for Auto-Report - Clamp 2 (W)\"\n            break\n        case 7:\n            parameterDescription = \"Power Change Threshold for Auto-Report - Clamp 3 (W)\"\n            break\n        case 8:\n            parameterDescription = \"Power Percentage Change Threshold for Auto-Report - Whole HEM (%)\"\n            break\n        case 9:\n            parameterDescription = \"Power Percentage Change Threshold for Auto-Report - Clamp 1 (%)\"\n            break\n        case 10:\n            parameterDescription = \"Power Percentage Change Threshold for Auto-Report - Clamp 2 (%)\"\n            break\n        case 11:\n            parameterDescription = \"Power Percentage Change Threshold for Auto-Report - Clamp 3 (%)\"\n            break\n        case 13:\n            parameterDescription = \"Enable Reporting CRC16 Encapsulation Command\"\n            break\n        case 101:\n            parameterDescription = \"Reporting Group 1 - Content Flags\"\n            break\n        case 102:\n            parameterDescription = \"Reporting Group 2 - Content Flags\"\n            break\n        case 103:\n            parameterDescription = \"Reporting Group 3 - Content Flags\"\n            break\n        case 111:\n            parameterDescription = \"Reporting Group 1 - Report Interval (s)\"\n            break\n        case 112:\n            parameterDescription = \"Reporting Group 2 - Report Interval (s)\"\n            break\n        case 113:\n            parameterDescription = \"Reporting Group 3 - Report Interval (s)\"\n            break\n        case 200:\n            parameterDescription = \"Partner ID\"\n            break\n        case 252:\n            parameterDescription = \"Configuration Locked\"\n            break\n        default:\n            parameterDescription = \"Unknown Parameter\"\n\t}\n    \n\t//log.debug \"$device.displayName: Configuration Report: parameterNumber: $cmd.parameterNumber, parameterDescription: $parameterDescription, size: $cmd.size, scaledConfigurationValue: $scValue\"\n\tcreateEvent(descriptionText: \"$device.displayName: Configuration Report: parameterNumber: $cmd.parameterNumber, parameterDescription: $parameterDescription, size: $cmd.size, scaledConfigurationValue: $scValue\", displayed: false)\n}\n\n/**\n *  COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 (0x72)\n *\n *  \n **/\ndef zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {\n\tdef msr = String.format(\"%04X-%04X-%04X\", cmd.manufacturerId, cmd.productTypeId, cmd.productId)\n\tif (state.debug) log.debug \"$device.displayName: MSR: $msr\"\n\tupdateDataValue(\"MSR\", msr)\n\n\t// Apply Manufacturer- or Product-specific configuration here...\n}\n\n/**\n *  Default event handler.\n *\n *  Called for all events that aren't handled above.\n **/\ndef zwaveEvent(physicalgraph.zwave.Command cmd) {\n\tif (state.debug) log.warn \"$device.displayName: Unhandled: $cmd\"\n\t[:]\n}\n\n\n/**********************************************************************\n *  Capability-related Commands:\n **********************************************************************/\n\n\n/**\n *  refresh() - Refreshes values from the device.\n *\n *  Required for the \"Refresh\" capability.\n **/\ndef refresh() {\n\tdelayBetween([\n\t\tzwave.meterV3.meterGet(scale: 0).format(), // Energy\n\t\tzwave.meterV3.meterGet(scale: 2).format(), // Power\n\t\tzwave.meterV3.meterGet(scale: 4).format(), // Volts\n\t\t//zwave.meterV3.meterGet(scale: 5).format(), // Current - Not included, as a request will be triggered when Power report is received.\n\t\t//zwave.meterV3.meterGet(scale: 6).format() // Power Factor - Not Supported.\n\t])\n}\n\n\n/**\n *  poll() - Polls the device.\n *\n *  Required for the \"Polling\" capability\n **/\ndef poll() {\n\trefresh()\n}\n\n\n/**\n *  reset() - Reset the Accumulated Energy figure held in the device.\n *\n *  Custom energy reporting period stats are preserved.\n **/\ndef reset() {\n\tif (state.debug) log.debug \"Reseting Accumulated Energy\"\n    state.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"lastReset\", value: state.lastReset, unit: \"\")\n\t\n\t// Record energy<Period> in energy<Period>Prev:\n\tstate.energyTodayPrev = state.energyToday\n\tstate.energyTodayStart = 0.00\n\tstate.energyMonthPrev = state.energyMonth\n\tstate.energyMonthStart = 0.00\n\tstate.energyYearPrev = state.energyYear\n\tstate.energyYearStart = 0.00\n    state.energyLifetimePrev = state.energyLifetime\n\tstate.energy = 0.00\n    \n    return [\n\t\tzwave.meterV3.meterReset().format(),\n\t\tzwave.meterV3.meterGet(scale: 0).format()\n\t]\n}\n\n\n/**********************************************************************\n *  Other Commands:\n **********************************************************************/\n\n\n/**\n *  resetAllStats() - Reset all Accumulated Energy statistics (!)\n *\n *  Resets the Accumulated Energy figure held in the device AND resets all custom energy reporting period stats!\n **/\ndef resetAllStats() {\n\tif (state.debug) log.debug \"Reseting All Accumulated Energy Stats!\"\n    state.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"lastReset\", value: state.lastReset, unit: \"\")\n\t\n\t// Reset all energy<Period>Prev/Start values:\n\tstate.energyTodayPrev = 0.00\n\tstate.energyTodayStart = 0.00\n\tstate.energyMonthPrev = 0.00\n\tstate.energyMonthStart = 0.00\n\tstate.energyYearPrev = 0.00\n\tstate.energyYearStart = 0.00\n    state.energyLifetimePrev = 0.00\n\tstate.energy = 0.00\n    \n    return [\n\t\tzwave.meterV3.meterReset().format(),\n\t\tzwave.meterV3.meterGet(scale: 0).format()\n\t]\n}\n\n\n/**\n *  installed() - Runs when the device is first installed.\n **/\ndef installed() {\n\tlog.debug \"${device.displayName}: Installing.\"\n\tstate.installedAt = now()\n\tstate.energy = 0.00\n\tstate.costPerKWH = 0.00\n\tstate.costOfEnergy = 0.00\n\tstate.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    state.statsMode = 'Today'\n}\n\n\n/**\n *  updated() - Runs when you hit \"Done\" from \"Edit Device\".\n * \n *  Weirdly, it seems to be called twice after hitting \"Done\"!\n * \n *  Note, the updated() method is not a 'command', so it doesn't send commands by default.\n *  To execute commands from updated() you have to specifically return a HubAction object. \n *  The response() helper wraps commands up in a HubAction so they can be sent from parse() or updated().\n *  See: https://community.smartthings.com/t/remotec-z-thermostat-configuration-with-z-wave-commands/31956/12\n **/\ndef updated() {\n\n\tlog.debug \"Updated() called\"\n\t// Update internal state:\n\tstate.debug = (\"true\" == configDebugMode)\n\tstate.costPerKWH = configCostPerKWH as BigDecimal\n    \n    // Call configure() and refresh():\n \treturn response( [configure() , refresh() ])\n}\n\n\n\n/**\n *  updateStats() - Recalculates energy and cost for each reporting period.\n *\n *  All costs are calculated at the prevailing rate.\n *\n *   Attributes:\n *    energy                = Energy (kWh) as reported by device (ad hoc period). [Native Energy Meter attribute].\n *    costOfEnergy          = Cost of energy (ad hoc period).\n *    energyToday           = Accumulated energy (today only).\n *    costOfEnergyToday     = Cost of energy (today).\n *    energy24Hours         = Accumulated energy (last 24 hours).\n *    costOfEnergy24Hours   = Cost of energy (last 24 hours).\n *    energy7Days           = Accumulated energy (last 7 days).\n *    costOfEnergy7Days     = Cost of energy (last 7 days).\n *    energyMonth           = Accumulated energy (this month).\n *    costOfEnergyMonth     = Cost of energy (this month).\n *    energyYear            = Accumulated energy (this year).\n *    costOfEnergyYear      = Cost of energy (this year).\n *    energyLifetime        = Accumulated energy (lifetime).\n *    costOfEnergyLifetime  = Cost of energy (lifetime).\n *   \n *   Private State:\n *    costPerKWH            = Unit cost as specified by user in settings.\n *    reportingPeriod       = YYYY/MM/dd of current reporting period.\n *    energyTodayStart      = energy that was reported at the start of today. Will be zero if ad hoc period has been reset today.\n *    energyTodayPrev       = energy that was reported today, prior to lastReset. Will be zero if ad hoc period has not been reset today.\n *    energyMonthStart      = energy that was reported at the start of this month. Will be zero if ad hoc period has been reset this month.\n *    energyMonthPrev       = energy that was reported this month, prior to lastReset. Will be zero if ad hoc period has not been reset this month.\n *    energyYearStart       = energy that was reported at the start of this year. Will be zero if ad hoc period has been reset this year.\n *    energyYearPrev        = energy that was reported this year, prior to lastReset. Will be zero if ad hoc period has not been reset this year.\n *    energyLifetimePrev    = energy that was reported this lifetime, prior to lastReset. Will be zero if ad hoc period has never been reset.\n *   \n **/\nprivate updateStats() {\n\n\tif (state.debug) log.debug \"${device.displayName}: Updating Statistics\"\n\t\n\tif (!state.energy) {state.energy = 0}\n\tif (!state.costPerKWH) {state.costPerKWH = 0}\n\tif (!state.reportingPeriod) {state.reportingPeriod = \"Uninitialised\"}\n\tif (!state.energyTodayStart) {state.energyTodayStart = 0}\n\tif (!state.energyTodayPrev) {state.energyTodayPrev = 0}\n\tif (!state.energyMonthStart) {state.energyMonthStart = 0}\n\tif (!state.energyMonthPrev) {state.energyMonthPrev = 0}\n\tif (!state.energyYearStart) {state.energyYearStart = 0}\n\tif (!state.energyYearPrev) {state.energyYearPrev = 0}\n\tif (!state.energyLifetimePrev) {state.energyLifetimePrev = 0}\n\t\n\t// Check if reportingPeriod has changed (i.e. it's a new day):\n\tdef today = new Date().format(\"YYYY/MM/dd\", location.timeZone)\n\tif ( today != state.reportingPeriod) {\n\t\t// It's a new Reporting Period:\n\t\tlog.info \"${device.displayName}: New Reporting Period: ${today}\"\n        \n        // Check if new year:\n\t\tif ( today.substring(0,4) != state.reportingPeriod.substring(0,4)) {\n        \tstate.energyYearStart = state.energy\n\t\t\tstate.energyYearPrev = 0.00\n        }\n\n        // Check if new month:\n\t\tif ( today.substring(0,7) != state.reportingPeriod.substring(0,7)) {\n        \tstate.energyMonthStart = state.energy\n\t\t\tstate.energyMonthPrev = 0.00\n        }\n\n        // Daily rollover:\n\t\tstate.energyTodayStart = state.energy\n\t\tstate.energyTodayPrev = 0.00\n        \n        // Update reportingPeriod:\n        state.reportingPeriod = today\n\t}\n\t\n    // energy (ad hoc period):\n    // Nothing to caclulate, just need to update dispEnergy:\n    sendEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",state.energy as BigDecimal) + \" kWh\", displayed: false)\n    \n    // costOfEnergy (ad hoc period):\n\ttry {\n\t\tstate.costOfEnergy = state.energy * state.costPerKWH\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy: £${state.costOfEnergy}\"\n\t\tsendEvent(name: \"costOfEnergy\", value: state.costOfEnergy, unit: \"£\")\n        sendEvent(name: \"dispCostOfEnergy\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy as BigDecimal), displayed: false)\n\t} catch (e) { log.debug e }\n\n\t// energyToday:\n\ttry {\n\t\tstate.energyToday = state.energy + state.energyTodayPrev - state.energyTodayStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy Today: ${state.energyToday} kWh\"\n\t\tsendEvent(name: \"energyToday\", value: state.energyToday, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyToday:\n\ttry {\n\t\tstate.costOfEnergyToday = (state.energyToday * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Today: £${state.costOfEnergyToday}\"\n\t\tsendEvent(name: \"costOfEnergyToday\", value: state.costOfEnergyToday, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyMonth:\n\ttry {\n\t\tstate.energyMonth = state.energy + state.energyMonthPrev - state.energyMonthStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Month: ${state.energyMonth} kWh\"\n\t\tsendEvent(name: \"energyMonth\", value: state.energyMonth, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyMonth:\n\ttry {\n\t\tstate.costOfEnergyMonth = (state.energyMonth * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Month: £${state.costOfEnergyMonth}\"\n\t\tsendEvent(name: \"costOfEnergyMonth\", value: state.costOfEnergyMonth, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyYear:\n\ttry {\n\t\tstate.energyYear = state.energy + state.energyYearPrev - state.energyYearStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Year: ${state.energyYear} kWh\"\n\t\tsendEvent(name: \"energyYear\", value: state.energyYear, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyYear:\n\ttry {\n\t\tstate.costOfEnergyYear = (state.energyYear * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Year: £${state.costOfEnergyYear}\"\n\t\tsendEvent(name: \"costOfEnergyYear\", value: state.costOfEnergyYear, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyLifetime:\n\ttry {\n\t\tstate.energyLifetime = state.energy + state.energyLifetimePrev\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Lifetime: ${state.energyLifetime} kWh\"\n\t\tsendEvent(name: \"energyLifetime\", value: state.energyLifetime, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyLifetime:\n\ttry {\n\t\tstate.costOfEnergyLifetime = (state.energyLifetime * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Lifetime: £${state.costOfEnergyLifetime}\"\n\t\tsendEvent(name: \"costOfEnergyLifetime\", value: state.costOfEnergyLifetime, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    // Moving Periods - Calculated by looking up previous values of energyLifetime:\n    \n    // energy24Hours:\n\ttry {\n    \t// We need the last value of energyLifetime that is at least 24 hours old.\n\t\t//  We get previous values of energyLifetime between 1 and 7 days old, in case the device has been off for a while.\n\t\t//  So long as the device reported energy back at least once during this period, we should get a result.\n        //  As results are returned in reverse chronological order, we just need the first 1 record.\n\t\t\n        // Use a calendar object to create offset dates:\n\t\tCalendar cal = new GregorianCalendar()\n\t\tcal.add(Calendar.DATE, -1 )\n\t\tDate end = cal.getTime()\n\t\tcal.add(Calendar.DATE, -6 )\n\t\tDate start = cal.getTime()\n\n\t\tdef previousELStates = device.statesBetween(\"energyLifetime\", start, end,[max: 1])\n\t\tdef previousEL\n    \tif (previousELStates) { \n        \tpreviousEL = previousELStates[previousELStates.size -1].value as BigDecimal \n            if (state.debug) log.debug \"${device.displayName}: energyLifetime 24 Hours Ago was: ${previousEL} kWh\"\n        }\n    \telse { \n        \tpreviousEL = 0.0 \n        \tif (state.debug) log.debug \"${device.displayName}: No value for energyLifetime 24 Hours Ago!\"\n        }\n    \tif (previousEL > state.energyLifetime) { previousEL = 0.0 } // If energyLifetime has been reset, discard previous value.\n        \n    \tstate.energy24Hours = state.energyLifetime - previousEL\n        if (state.debug) log.debug \"${device.displayName}: Energy Last 24 Hours: ${state.energy24Hours} kWh\"\n\t\tsendEvent(name: \"energy24Hours\", value: state.energy24Hours, unit: \"kWh\")\n\t} catch (e) { log.debug e }    \n    \n\t// costOfEnergy24Hours:\n\ttry {\n\t\tstate.costOfEnergy24Hours = (state.energy24Hours * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Last 24 Hours: £${state.costOfEnergy24Hours}\"\n\t\tsendEvent(name: \"costOfEnergy24Hours\", value: state.costOfEnergy24Hours, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    \n    // energy7Days:\n\ttry {\n    \t// We need the last value of energyLifetime, up to 7 old (previous states are only kept for 7 days).\n\t\t//  We get previous values of energyLifetime between 6 and 7 days old, in case the device has been off for a while.\n\t\t//  So long as the device reported energy back at least once during this period, we should get a result.\n        //  As results are returned in reverse chronological order, we need the last record, so we request the max of 1000.\n\t\t//  If there were more than 1000 updates between start and end, we won't get the oldest one,\n        //  however stats should normally only be generated every 10 mins at most.\n\t\t\n    \t// Use a calendar object to create offset dates:\n\t\tCalendar cal = new GregorianCalendar()\n\t\tcal.add(Calendar.DATE, -6 )\n\t\tDate end = cal.getTime()\n\t\tcal.add(Calendar.DATE, -1 )\n\t\tDate start = cal.getTime()\n\n\t\t// Get previous values of energyLifetime between 7 Days and 6 days 23 hours old: \n\t\tdef previousELStates = device.statesBetween(\"energyLifetime\", start, end,[max: 1000])\n\t\tdef previousEL\n    \tif (previousELStates) { \n        \tpreviousEL = previousELStates[previousELStates.size -1].value as BigDecimal \n            if (state.debug) log.debug \"${device.displayName}: energyLifetime 7 Days Ago was: ${previousEL} kWh\"\n        }\n    \telse { \n        \tpreviousEL = 0.0 \n        \tif (state.debug) log.debug \"${device.displayName}: No value for energyLifetime 7 Days Ago!\"\n        }\n    \tif (previousEL > state.energyLifetime) { previousEL = 0.0 } // If energyLifetime has been reset, discard previous value.\n        \n    \tstate.energy7Days = state.energyLifetime - previousEL\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy Last 7 Days: ${state.energy7Days} kWh\"\n\t\tsendEvent(name: \"energy7Days\", value: state.energy7Days, unit: \"kWh\")\n\t} catch (e) { log.debug e }    \n    \n\t// costOfEnergy7Days:\n\ttry {\n\t\tstate.costOfEnergy7Days = (state.energy7Days * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Last 7 Days: £${state.costOfEnergy7Days}\"\n\t\tsendEvent(name: \"costOfEnergy7Days\", value: state.costOfEnergy7Days, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    \n    //disp<>Period:\n    if ('Today' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyToday as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyToday as BigDecimal), displayed: false)\n    }\n    if ('Last 24 Hours' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy24Hours as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy24Hours as BigDecimal), displayed: false)\n    }\n    if ('Last 7 Days' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy7Days as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy7Days as BigDecimal), displayed: false)\n    }\n    if ('This Month' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyMonth as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyMonth as BigDecimal), displayed: false)\n    }\n    if ('This Year' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyYear as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyYear as BigDecimal), displayed: false)\n    }\n    if ('Lifetime' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyLifetime as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyLifetime as BigDecimal), displayed: false)\n    }\n    \n}\n\n\n/**\n *  cycleStats() - Cycle displayed statistics period.\n **/\ndef cycleStats() {\n\tif (state.debug) log.debug \"$device.displayName: Cycling Stats\"\n\t\n    if ('Today' == state.statsMode) {\n    \tstate.statsMode = 'Last 24 Hours'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy24Hours as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy24Hours as BigDecimal), displayed: false)\n    }\n    else if ('Last 24 Hours' == state.statsMode) {\n    \tstate.statsMode = 'Last 7 Days'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy7Days as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy7Days as BigDecimal), displayed: false)\n    }\n    else if ('Last 7 Days' == state.statsMode) {\n    \tstate.statsMode = 'This Month'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyMonth as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyMonth as BigDecimal), displayed: false)\n    }\n    else if ('This Month' == state.statsMode) {\n    \tstate.statsMode = 'This Year'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyYear as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyYear as BigDecimal), displayed: false)\n    }\n    else if ('This Year' == state.statsMode) {\n    \tstate.statsMode = 'Lifetime'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyLifetime as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyLifetime as BigDecimal), displayed: false)\n    }\n    else  {\n    \tstate.statsMode = 'Today'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyToday as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyToday as BigDecimal), displayed: false)\n    }\n    \n\tsendEvent(name: \"statsMode\", value: state.statsMode, displayed: false)\n\tif (state.debug) log.debug \"$device.displayName: StatsMode changed to: ${state.statsMode}\"\n\t\n}\n\n\n/**\n *  configure() - Configure physical device parameters.\n *\n *  Gets values from the Preferences section.\n **/\ndef configure() {\n    \n    if (state.debug) log.debug \"$device.displayName: Configuring Device\"\n    \n    // Build Commands based on input preferences:\n    // Some basic validation is done, if any values are out of range they're set back to default.\n    //  It doesn't seem possible to read the defaultValue of each input from $settings, so default values are duplicated here.\n    def cmds = []\n    \n\t// Parameter 2 - Energy Detection Mode:\n\tShort CP2 \n    if (configEnergyDetectionMode == \"Wattage, absolute kWh\") {CP2 = 0}\n\telse if (configEnergyDetectionMode == \"+/-Wattage, algebraic sum kWh\") {CP2 = 1}\n\telse if (configEnergyDetectionMode == \"+/-Wattage, +ive kWh (consuming electricity)\") {CP2 = 2}\n\telse if (configEnergyDetectionMode == \"+/-Wattage, -iv kWh (generating electricity)\") {CP2 = 3}\n\telse {CP2 = 0}\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: CP2).format() \n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format()\n\t\n\t// Parameter 3 - Selective Reporting:\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: (\"true\" == configSelectiveReporting) ? 1 : 0).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format()\t\t\n\t\n    // Parameter 4 - Power Change Threshold for Auto-Report - Whole HEM (W):\n\tLong CP4 = settings.configPowerThresholdAbs_HEM as Long  \n    if ((CP4 == null) || (CP4 < 0) || (CP4 > 60000)) { CP4 = 50 }\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 2, scaledConfigurationValue: CP4).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 4).format()\n\n    // Parameter 8 - Power Percentage Change Threshold for Auto-Report - Whole HEM (%):\n    Long CP8 = settings.configPowerThresholdPercent_HEM as Long  \n    if ((CP8 == null) || (CP8 < 0) || (CP8 > 100)) { CP8 = 10 }\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 8, size: 1, scaledConfigurationValue: CP8).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 8).format()\n\n\t// Reporting Group Flags:\n\t//  energy = 1\n\t//  power = 2\n\t//  voltage = 4\n\t//  current = 8\n        \n    // Parameter 101 - Reporting Group 1 - Content Flags:\n\t// HARD-CODED to contain power (W) and current [2+8 = 10]:\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 10).format()\n\tcmds << zwave.configurationV1.configurationGet(parameterNumber: 101).format()\n\t\n\t// Parameter 102 - Reporting Group 2 - Content Flags:\n\t// HARD-CODED to contain energy [1]:\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1).format()\n\tcmds << zwave.configurationV1.configurationGet(parameterNumber: 102).format()\n\t\n\t// Parameter 103 - Reporting Group 3 - Content Flags:\n\t// HARD-CODED to contain voltage [4]:\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 4).format()\n\tcmds << zwave.configurationV1.configurationGet(parameterNumber: 103).format()\n\t\n\t\n\t// Parameter 111 - Reporting Group 1 - Report Interval (s):\n\tLong CP111 = settings.configReportGroup1Interval as Long  \n    if ((CP111 == null) || (CP111 < 1) || (CP111 > 2147483647)) { CP111 = 60 }\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: CP111).format()\n\tcmds << zwave.configurationV1.configurationGet(parameterNumber: 111).format()\n\t\n\t// Parameter 112 - Reporting Group 2 - Report Interval (s):\n\tLong CP112 = settings.configReportGroup2Interval as Long  \n    if ((CP112 == null) || (CP112 < 1) || (CP112 > 2147483647)) { CP112 = 600 }\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: CP112).format()\n\tcmds << zwave.configurationV1.configurationGet(parameterNumber: 112).format()\n\t\n\t// Parameter 113 - Reporting Group 3 - Report Interval (s):\n\tLong CP113 = settings.configReportGroup3Interval as Long  \n    if ((CP113 == null) || (CP113 < 1) || (CP113 > 2147483647)) { CP113 = 600 }\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: CP113).format()\n\tcmds << zwave.configurationV1.configurationGet(parameterNumber: 113).format()\n\t\n\t\n    // Return:\n    if ( cmds != [] && cmds != null ) return delayBetween(cmds, 500) else return\n}\n\n\n/**\n *  test() - Temp testing method.\n **/\ndef test() {\n\tif (state.debug) log.debug \"${device.displayName}: Testing\"\n}\n\n"
  },
  {
    "path": "devices/evohome/evohome-heating-zone.groovy",
    "content": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: Evohome Heating Zone\n *\n *  Author: David Lomas (codersaur)\n *\n *  Date: 2016-04-08\n *\n *  Version: 0.09\n *\n *  Description:\n *   - This device handler is a child device for the Evohome (Connect) SmartApp.\n *   - For latest documentation see: https://github.com/codersaur/SmartThings\n *\n *  Version History:\n *\n *   2016-04-08: v0.09\n *    - calculateOptimisations(): Fixed comparison of temperature values.\n * \n *   2016-04-05: v0.08\n *    - New 'Update Refresh Time' setting from parent to control polling after making an update.\n *    - setThermostatMode(): Forces poll for all zones to ensure new thermostatMode is updated.\n * \n *   2016-04-04: v0.07\n *    - generateEvent(): hides events if name or value are null.\n *    - generateEvent(): log.info message for new values.\n * \n *   2016-04-03: v0.06\n *    - Initial Beta Release\n * \n *  To Do:\n *   - Clean up device settings (preferences). Hide/Show prefSetpointDuration input dynamically depending on prefSetpointMode. - If supported for devices???\n *   - When thermostat mode is away or off, heatingSetpoint overrides should not allowed (although setting while away actually works). Should warn at least.\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n */ \nmetadata {\n\tdefinition (name: \"Evohome Heating Zone\", namespace: \"codersaur\", author: \"David Lomas\") {\n\t\tcapability \"Actuator\"\n\t\tcapability \"Sensor\"\n\t\tcapability \"Refresh\"\n\t\tcapability \"Temperature Measurement\"\n\t\tcapability \"Thermostat\"\n\t\t\n\t\t//command \"poll\" // Polling\n\t\tcommand \"refresh\" // Refresh\n\t\tcommand \"setHeatingSetpoint\" // Thermostat\n\t\tcommand \"raiseSetpoint\" // Custom\n\t\tcommand \"lowerSetpoint\" // Custom\n\t\tcommand \"setThermostatMode\" // Thermostat\n\t\tcommand \"cycleThermostatMode\" // Custom\n\t\tcommand \"off\" // Thermostat\n\t\tcommand \"heat\" // Thermostat\n\t\tcommand \"auto\" // Custom\n\t\tcommand \"away\" // Custom\n\t\tcommand \"economy\" // Custom\n\t\tcommand \"dayOff\" // Custom\n\t\tcommand \"custom\" // Custom\n\t\tcommand \"resume\" // Custom\n\t\tcommand \"boost\" // Custom\n\t\tcommand \"suppress\" // Custom\n\t\tcommand \"generateEvent\" // Custom\n\t\tcommand \"test\" // Custom\n\n\t\tattribute \"temperature\",\"number\" // Temperature Measurement\n\t\tattribute \"heatingSetpoint\",\"number\" // Thermostat\n\t\tattribute \"thermostatSetpoint\",\"number\" // Thermostat\n\t\tattribute \"thermostatSetpointMode\", \"string\" // Custom\n\t\tattribute \"thermostatSetpointUntil\", \"string\" // Custom\n\t\tattribute \"thermostatSetpointStatus\", \"string\" // Custom\n\t\tattribute \"thermostatMode\", \"string\" // Thermostat\n\t\tattribute \"thermostatOperatingState\", \"string\" // Thermostat\n\t\tattribute \"thermostatStatus\", \"string\" // Custom\n\t\tattribute \"scheduledSetpoint\", \"number\" // Custom\n\t\tattribute \"nextScheduledSetpoint\", \"number\" // Custom\n\t\tattribute \"nextScheduledTime\", \"string\" // Custom\n\t\tattribute \"optimisation\", \"string\" // Custom\n\t\tattribute \"windowFunction\", \"string\" // Custom\n\t\t\n\t}\n\n\ttiles(scale: 2) {\n\n\t\t// Main multi\n\t\tmultiAttributeTile(name:\"multi\", type:\"thermostat\", width:6, height:4) {\n\t\t\ttileAttribute(\"device.temperature\", key: \"PRIMARY_CONTROL\") {\n\t\t\t\tattributeState(\"default\", label:'${currentValue}', unit:\"C\")\n\t\t\t}\n\t\t\t// Up and Down buttons:\n\t\t\t//tileAttribute(\"device.temperature\", key: \"VALUE_CONTROL\") {\n\t\t\t//\tattributeState(\"VALUE_UP\", action: \"raiseSetpoint\")\n\t\t\t//\tattributeState(\"VALUE_DOWN\", action: \"lowerSetpoint\")\n\t\t\t//}\n\t\t\t// Operating State - used to get background colour when type is 'thermostat'.\n\t\t\ttileAttribute(\"device.thermostatStatus\", key: \"OPERATING_STATE\") {\n\t\t\t\tattributeState(\"Heating\", backgroundColor:\"#ffa81e\", defaultState: true)\n\t\t\t\tattributeState(\"Idle (Auto)\", backgroundColor:\"#44b621\")\n\t\t\t\tattributeState(\"Idle (Custom)\", backgroundColor:\"#44b621\")\n\t\t\t\tattributeState(\"Idle (Day Off)\", backgroundColor:\"#44b621\")\n\t\t\t\tattributeState(\"Idle (Economy)\", backgroundColor:\"#44b621\")\n\t\t\t\tattributeState(\"Idle (Away)\", backgroundColor:\"#44b621\")\n\t\t\t\tattributeState(\"Off\", backgroundColor:\"#269bd2\")\n\t\t\t}\n\t\t\t//tileAttribute(\"device.thermostatMode\", key: \"THERMOSTAT_MODE\") {\n\t\t\t//\tattributeState(\"off\", label:'${name}')\n\t\t\t//\tattributeState(\"away\", label:'${name}')\n\t\t\t//\tattributeState(\"auto\", label:'${name}')\n\t\t\t//\tattributeState(\"economy\", label:'${name}')\n\t\t\t//\tattributeState(\"dayOff\", label:'${name}')\n\t\t\t//\tattributeState(\"custom\", label:'${name}')\n\t\t\t//}\n\t\t\t//tileAttribute(\"device.heatingSetpoint\", key: \"HEATING_SETPOINT\") {\n\t\t\t//\tattributeState(\"default\", label:'${currentValue}', unit:\"C\")\n\t\t\t//}\n\t\t\t//tileAttribute(\"device.coolingSetpoint\", key: \"COOLING_SETPOINT\") {\n\t\t\t//\tattributeState(\"default\", label:'${currentValue}', unit:\"C\")\n\t\t\t//}\n\t\t}\n\t\n\t\t// temperature tile:\n\t\tvalueTile(\"temperature\", \"device.temperature\", width: 2, height: 2, canChangeIcon: true) {\n\t\t\tstate(\"temperature\", label:'${currentValue}', unit:\"C\", icon:\"st.Weather.weather2\",\n\t\t\t\t\tbackgroundColors:[\n\t\t\t\t\t\t\t// Celsius\n\t\t\t\t\t\t\t[value: 0, color: \"#153591\"],\n\t\t\t\t\t\t\t[value: 7, color: \"#1e9cbb\"],\n\t\t\t\t\t\t\t[value: 15, color: \"#90d2a7\"],\n\t\t\t\t\t\t\t[value: 23, color: \"#44b621\"],\n\t\t\t\t\t\t\t[value: 28, color: \"#f1d801\"],\n\t\t\t\t\t\t\t[value: 35, color: \"#d04e00\"],\n\t\t\t\t\t\t\t[value: 37, color: \"#bc2323\"]\n\t\t\t\t\t]\n\t\t\t)\n\t\t}\n\t\t\n\t\t// thermostatSetpoint tiles:\n\t\tvalueTile(\"thermostatSetpoint\", \"device.thermostatSetpoint\", width: 3, height: 1) {\n\t\t\tstate \"thermostatSetpoint\", label:'Setpoint: ${currentValue}', unit:\"C\"\n\t\t}\n\t\tvalueTile(\"thermostatSetpointStatus\", \"device.thermostatSetpointStatus\", width: 3, height: 1, decoration: \"flat\") {\n\t\t\tstate \"thermostatSetpointStatus\", label:'${currentValue}', backgroundColor:\"#ffffff\"\n\t\t}\n\t\tstandardTile(\"raiseSetpoint\", \"device.thermostatSetpoint\", width: 1, height: 1, decoration: \"flat\") {\n\t\t\tstate \"setpoint\", action:\"raiseSetpoint\", icon:\"st.thermostat.thermostat-up\"\n\t\t}\n\t\tstandardTile(\"lowerSetpoint\", \"device.thermostatSetpoint\", width: 1, height: 1, decoration: \"flat\") {\n\t\t\tstate \"setpoint\", action:\"lowerSetpoint\", icon:\"st.thermostat.thermostat-down\"\n\t\t}\n\t\tstandardTile(\"resume\", \"device.resume\", width: 1, height: 1, decoration: \"flat\") {\n\t\t\tstate \"default\", action:\"resume\", label:'Resume', icon:\"st.samsung.da.oven_ic_send\"\n\t\t}\n\t\tstandardTile(\"boost\", \"device.boost\", inactiveLabel: false, decoration: \"flat\", width: 1, height: 1) {\n\t\t\tstate \"default\", action:\"boost\", label:'Boost' // icon TBC\n\t\t}\n\t\tstandardTile(\"suppress\", \"device.suppress\", inactiveLabel: false, decoration: \"flat\", width: 1, height: 1) {\n\t\t\tstate \"default\", action:\"suppress\", label:'Suppress' // icon TBC\n\t\t}\n\t\t\n\t\t\n\t\t// thermostatMode/Status Tiles:\n\t\t\n\t\t// thermostatStatus (also incorporated into the multi tile).\n\t\tvalueTile(\"thermostatStatus\", \"device.thermostatStatus\", height: 1, width: 6, decoration: \"flat\") {\n\t\t\tstate \"thermostatStatus\", label:'${currentValue}', backgroundColor:\"#ffffff\"\n\t\t}\n\t\t// Single thermostatMode tile that cycles between all modes (too slow).\n\t\t// To Do: Update with Evohome-specific modes:\n\t\tstandardTile(\"thermostatMode\", \"device.thermostatMode\", inactiveLabel: false, decoration: \"flat\") {\n\t\t\tstate \"off\", action:\"cycleMode\", nextState: \"updating\", icon: \"st.thermostat.heating-cooling-off\"\n\t\t\tstate \"heat\", action:\"cycleMode\",  nextState: \"updating\", icon: \"st.thermostat.heat\"\n\t\t\tstate \"cool\", action:\"cycleMode\",  nextState: \"updating\", icon: \"st.thermostat.cool\"\n\t\t\tstate \"auto\", action:\"cycleMode\",  nextState: \"updating\", icon: \"st.thermostat.auto\"\n\t\t\tstate \"auxHeatOnly\", action:\"cycleMode\", icon: \"st.thermostat.emergency-heat\"\n\t\t\tstate \"updating\", label:\"Working\", icon: \"st.secondary.secondary\"\n\t\t}\n\t\t// Individual Mode tiles:\n\t\tstandardTile(\"auto\", \"device.auto\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", action:\"auto\", icon: \"st.thermostat.auto\"\n\t\t}\n\t\tstandardTile(\"away\", \"device.away\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", action:\"away\", label:'Away' // icon TBC\n\t\t}\n\t\tstandardTile(\"custom\", \"device.custom\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", action:\"custom\", label:'Custom' // icon TBC\n\t\t}\n\t\tstandardTile(\"dayOff\", \"device.dayOff\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", action:\"dayOff\", label:'Day Off' // icon TBC\n\t\t}\n\t\tstandardTile(\"economy\", \"device.economy\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", action:\"economy\", label:'Economy' // icon TBC\n\t\t}\n\t\tstandardTile(\"off\", \"device.off\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", action:\"off\", icon:\"st.thermostat.heating-cooling-off\"\n\t\t}\n\t\t// Other tiles:\n\t\tstandardTile(\"refresh\", \"device.thermostatMode\", inactiveLabel: false, decoration: \"flat\") {\n\t\t\tstate \"default\", action:\"refresh.refresh\", icon:\"st.secondary.refresh\"\n\t\t}\n\t\tstandardTile(\"test\", \"device.test\", width: 1, height: 1, decoration: \"flat\") {\n\t\t\tstate \"default\", label:'Test', action:\"test\"\n\t\t}\n\t\t\n\t\tmain \"temperature\"\n\t\tdetails(\n\t\t\t\t[\n\t\t\t\t\"multi\",\n\t\t\t\t\"thermostatSetpoint\",\"raiseSetpoint\",\"boost\",\"resume\",\n\t\t\t\t\"thermostatSetpointStatus\",\"lowerSetpoint\",\"suppress\",\"refresh\",\n\t\t\t\t\"auto\",\"away\",\"custom\",\"dayOff\",\"economy\",\"off\"\n\t\t\t\t]\n\t\t)\n\t}\n\n\tpreferences {\n\t\tsection { // Setpoint Adjustments:\n\t\t\tinput title: \"Setpoint Duration\", description: \"Configure how long setpoint adjustments are applied for.\", displayDuringSetup: true, type: \"paragraph\", element: \"paragraph\"\n\t\t\tinput 'prefSetpointMode', 'enum', title: 'Until', description: '', options: [\"Next Switchpoint\", \"Midday\", \"Midnight\", \"Duration\", \"Permanent\"], defaultValue: \"Next Switchpoint\", required: true, displayDuringSetup: true\n\t\t\tinput 'prefSetpointDuration', 'number', title: 'Duration (minutes)', description: 'Apply setpoint for this many minutes', range: \"1..1440\", defaultValue: 60, required: true, displayDuringSetup: true\n\t\t\t//input 'prefSetpointTime', 'time', title: 'Time', description: 'Apply setpoint until this time', required: true, displayDuringSetup: true\n\t\t\tinput title: \"Setpoint Temperatures\", description: \"Configure preset temperatures for the 'Boost' and 'Suppress' buttons.\", displayDuringSetup: true, type: \"paragraph\", element: \"paragraph\"\n\t\t\tinput \"prefBoostTemperature\", \"string\", title: \"'Boost' Temperature\", defaultValue: \"21.5\", required: true, displayDuringSetup: true // use of 'decimal' input type in devices is currently broken.\n\t\t\tinput \"prefSuppressTemperature\", \"string\", title: \"'Suppress' Temperature\", defaultValue: \"15.0\", required: true, displayDuringSetup: true // use of 'decimal' input type in devices is currently broken.\n\t\t}\n\t\t\t\t\n\t}\n\n}\n\n/**********************************************************************\n *  Test Commands:\n **********************************************************************/\n\n\n/**\n *  test()\n *\n *  Test method, called from test tile.\n **/\ndef test() {\n\n\t//log.debug \"$device.displayName: test(): Properties: ${properties}\"\n\t//log.debug \"$device.displayName: test(): Settings: ${settings}\"\n\t//log.debug \"$device.displayName: test(): State: ${state}\"    \n\n}\n\n\n/**********************************************************************\n *  Setup and Configuration Commands:\n **********************************************************************/\n\n/**\n *  installed()\n *\n *  Runs when the app is first installed.\n *  \n *  When a device is created by a SmartApp, settings are not populated\n *  with the defaultValues configured for each input. Therefore, we\n *  populate the corresponding state.* variables with the input defaultValues.\n * \n **/\ndef installed() {\n\n\tlog.debug \"${app.label}: Installed with settings: ${settings}\"\n\n\tstate.installedAt = now()\n\t\n\t// These default values will be overwritten by the Evohome SmartApp almost immediately:\n\tstate.debug = false\n    state.updateRefreshTime = 5 // Wait this many seconds after an update before polling.\n\tstate.zoneType = 'RadiatorZone'\n\tstate.minHeatingSetpoint = formatTemperature(5.0)\n\tstate.maxHeatingSetpoint = formatTemperature(35.0)\n\tstate.temperatureResolution = formatTemperature(0.5)\n\tstate.windowFunctionTemperature = formatTemperature(5.0)\n\tstate.targetSetpoint = state.minHeatingSetpoint\n\t\n\t// Populate state.* with default values for each preference/input:\n\tstate.setpointMode = getInputDefaultValue('prefSetpointMode')\n\tstate.setpointDuration = getInputDefaultValue('prefSetpointDuration')\n\tstate.boostTemperature = getInputDefaultValue('prefBoostTemperature')\n\tstate.suppressTemperature = getInputDefaultValue('prefSuppressTemperature')\n\t\n}\n\n\n/**\n *  updated()\n * \n *  Runs when device settings are changed.\n **/\ndef updated() {\n\n\tif (state.debug) log.debug \"${device.label}: Updating with settings: ${settings}\"\n\n\t// Copy input values to state:\n\tstate.setpointMode = settings.prefSetpointMode\n\tstate.setpointDuration = settings.prefSetpointDuration\n\tstate.boostTemperature = formatTemperature(settings.prefBoostTemperature)\n\tstate.suppressTemperature = formatTemperature(settings.prefSuppressTemperature)\n\n}\n\n\n/**********************************************************************\n *  SmartApp-Child Interface Commands:\n **********************************************************************/\n\n/**\n *  generateEvent(values)\n *\n *  Called by parent to update the state of this child device.\n *\n **/\nvoid generateEvent(values) {\n\n\tlog.info \"${device.label}: generateEvent(): New values: ${values}\"\n\t\n\tif(values) {\n\t\tvalues.each { name, value ->\n\t\t\tif ( name == 'minHeatingSetpoint' \n\t\t\t\t|| name == 'maxHeatingSetpoint' \n\t\t\t\t|| name == 'temperatureResolution' \n\t\t\t\t|| name == 'windowFunctionTemperature'\n\t\t\t\t|| name == 'zoneType'\n\t\t\t\t|| name == 'locationId'\n\t\t\t\t|| name == 'gatewayId'\n\t\t\t\t|| name == 'systemId'\n\t\t\t\t|| name == 'zoneId'\n\t\t\t\t|| name == 'schedule'\n\t\t\t\t|| name == 'debug'\n                || name == 'updateRefreshTime'\n\t\t\t\t) {\n\t\t\t\t// Internal state only.\n\t\t\t\tstate.\"${name}\" = value\n\t\t\t}\n\t\t\telse { // Attribute value, so generate an event:\n\t\t\t\tif (name != null && value != null) {\n\t\t\t\t\tsendEvent(name: name, value: value, displayed: true)\n\t\t\t\t}\n\t\t\t\telse { // If name or value is null, set displayed to false,\n\t\t\t\t\t   // otherwise the 'Recently' view on smartphone app clogs \n\t\t\t\t\t   // up with empty events.\n\t\t\t\t\tsendEvent(name: name, value: value, displayed: false)\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Reset targetSetpoint (used by raiseSetpoint/lowerSetpoint) if heatingSetpoint has changed:\n\t\t\t\tif (name == 'heatingSetpoint') {\n\t\t\t\t\tstate.targetSetpoint = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Calculate derived attributes (order is important here):\n\tcalculateThermostatOperatingState()\n\tcalculateOptimisations()\n\tcalculateThermostatStatus()\n\tcalculateThermostatSetpointStatus()\n\t\n}\n\n\n/**********************************************************************\n *  Capability-related Commands:\n **********************************************************************/\n\n\n/**\n *  poll()\n *\n *  Polls the device. Required for the \"Polling\" capability\n **/\nvoid poll() {\n\n\tif (state.debug) log.debug \"${device.label}: poll()\"\n\tparent.poll(state.zoneId)\n}\n\n\n/**\n *  refresh()\n *\n *  Refreshes values from the device. Required for the \"Refresh\" capability.\n **/\nvoid refresh() {\n\n\tif (state.debug) log.debug \"${device.label}: refresh()\"\n\tsendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)\n\tparent.poll(state.zoneId)\n}\n\n\n/**\n *  setThermostatMode(mode, until=-1)\n * \n *  Set thermostat mode until specified time.\n *\n *   mode:    Possible values: 'auto','off','away','dayOff','custom', or 'economy'.\n *\n *   until:   (Optional) Time to apply mode until, can be either:\n *             - Date: Date object representing when override should end.\n *             - ISO-8601 date string, in format \"yyyy-MM-dd'T'HH:mm:ssXX\", e.g.: \"2016-04-01T00:00:00Z\".\n *             - String: 'permanent'.\n *             - Number: Duration in hours if mode is 'economy', or days if mode is 'away'/'dayOff'/'custom'.\n *                       Duration will be rounded down to align with Midnight i nthe local timezone\n *                       (e.g. a duration of 1 day will end at midnight tonight). If 0, mode is permanent.\n *                       If duration is not specified, a default value is used from the Evohome SmartApp settings.\n *\n *   Notes:   'Auto' and 'Off' modes are always permanent.\n *            Thermostat mode is a property of the temperatureControlSystem (i.e. Evohome controller).\n *            Therefore changing the thermostatMode will affect all zones associated with the same controller.\n * \n *  Example usage:\n *   setThermostatMode('off', 0)         // Set off mode permanently.\n *   setThermostatMode('away', 1)        // Set away mode for one day (i.e. until midnight tonight).\n *   setThermostatMode('dayOff', 2)      // Set dayOff mode for two days (ends tomorrow night).\n *   setThermostatMode('economy', 2)     // Set economy mode for two hours.\n *\n **/\ndef setThermostatMode(String mode, until=-1) {\n\n\tlog.info \"${device.label}: setThermostatMode(Mode: ${mode}, Until: ${until})\"\n\t\n\t// Send update via parent:\n\tif (!parent.setThermostatMode(state.systemId, mode, until)) {\n\t\tsendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)\n\t\t// Wait a few seconds as it takes a while for Evohome to update setpoints in response to a mode change.\n\t\tpseudoSleep(state.updateRefreshTime * 1000)\n\t\tparent.poll(0) // Force poll for all zones as thermostatMode is a property of the temperatureControlSystem.\n\t\treturn null\n\t}\n\telse {\n\t\tlog.error \"${device.label}: setThermostatMode(): Error: Unable to set thermostat mode.\"\n\t\treturn 'error'\n\t}\n}\n\n\n/**\n *  setHeatingSetpoint(setpoint, until=-1)\n * \n *  Set heatingSetpoint until specified time.\n *\n *   setpoint:   Setpoint temperature, e.g.: \"21.5\". Can be a number or string.\n *               If setpoint is outside allowed range (i.e. minHeatingSetpoint to \n *               maxHeatingSetpoint) it will be re-written to the appropriate limit.\n *\n *   until:      (Optional) Time to apply setpoint until, can be either:\n *                - Date: date object representing when override should end.\n *                - ISO-8601 date string, in format \"yyyy-MM-dd'T'HH:mm:ssXX\", e.g.: \"2016-04-01T00:00:00Z\".\n *                - String: 'nextSwitchpoint', 'midnight', 'midday', or 'permanent'.\n *                - Number: duration in minutes (from now). 0 = permanent.\n *               If not specified, setpoint duration will default to the\n *               behaviour defined in the device settings.\n *\n *  Example usage:\n *   setHeatingSetpoint(21.0)                           // Set until <device default>.\n *   setHeatingSetpoint(21.0, 'nextSwitchpoint')        // Set until next scheduled switchpoint.\n *   setHeatingSetpoint(21.0, 'midnight')               // Set until midnight.\n *   setHeatingSetpoint(21.0, 'permanent')              // Set permanently.\n *   setHeatingSetpoint(21.0, 0)                        // Set permanently.\n *   setHeatingSetpoint(21.0, 6)                        // Set for 6 hours.\n *   setHeatingSetpoint(21.0, '2016-04-01T00:00:00Z')   // Set until specific time.\n *\n **/\ndef setHeatingSetpoint(setpoint, until=-1) {\n\n\tif (state.debug) log.debug \"${device.label}: setHeatingSetpoint(Setpoint: ${setpoint}, Until: ${until})\"\n\t\n\t// Clean setpoint:\n\tsetpoint = formatTemperature(setpoint)\n\tif (Float.parseFloat(setpoint) < Float.parseFloat(state.minHeatingSetpoint)) {\n\t\tlog.warn \"${device.label}: setHeatingSetpoint(): Specified setpoint (${setpoint}) is less than zone's minimum setpoint (${state.minHeatingSetpoint}).\"\n\t\tsetpoint = state.minHeatingSetpoint\n\t}\n\telse if (Float.parseFloat(setpoint) > Float.parseFloat(state.maxHeatingSetpoint)) {\n\t\tlog.warn \"${device.label}: setHeatingSetpoint(): Specified setpoint (${setpoint}) is greater than zone's maximum setpoint (${state.maxHeatingSetpoint}).\"\n\t\tsetpoint = state.maxHeatingSetpoint\n\t}\n\t\n\t// Clean and parse until value:\n\tdef untilRes\n\tCalendar c = new GregorianCalendar()\n\tdef tzOffset = location.timeZone.getOffset(new Date().getTime()) // Timezone offset to UTC in milliseconds.\n\t\n\t// If until has not been specified, determine behaviour from device state.setpointMode:\n\tif (-1 == until) {\n\t\tswitch (state.setpointMode) {\n\t    \tcase 'Next Switchpoint':\n\t        \tuntil = 'nextSwitchpoint'\n\t            break\n\t    \tcase 'Midday':\n\t        \tuntil = 'midday'\n\t            break\n\t    \tcase 'Midnight':\n\t        \tuntil = 'midnight'\n\t            break\n\t    \tcase 'Duration':\n\t        \tuntil = state.setpointDuration ?: 0\n\t            break\n\t    \tcase 'Time':\n\t\t\t\t// TO DO : construct time, like we do for midnight.\n\t\t\t\t// settings.prefSetpointTime appears to return an ISO dateformat string.\n\t\t\t\t// However using an input of type \"time\" causes HTTP 500 errors in the IDE, so disabled for now.\n\t\t\t\t// If time has passed, then need to make it the next day.\n\t\t\t\tif (state.debug) log.debug \"${device.label}: setHeatingSetpoint(): Time: ${state.SetpointTime}\"\n\t        \tuntil = 'nextSwitchpoint'\n\t            break\n\t    \tcase 'Permanent':\n\t        \tuntil = 'permanent'\n\t            break\n\t    \tdefault:\n\t        \tuntil = 'nextSwitchpoint'\n\t            break\n\t\t}\n\t}\n\t\n\tif ('permanent' == until || 0 == until) {\n\t\tuntilRes = 0\n\t}\n\telse if (until instanceof Date) {\n\t\tuntilRes = until\n\t}\n\telse if ('nextSwitchpoint' == until) {\n\t\tuntilRes = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", device.currentValue('nextScheduledTime'))\n\t}\n\telse if ('midday' == until) {\n\t\tuntilRes = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", new Date().format(\"yyyy-MM-dd'T'12:00:00XX\", location.timeZone)) \n\t}\n\telse if ('midnight' == until) {\n\t\tc.add(Calendar.DATE, 1 ) // Add one day to calendar and use to get midnight in local time:\n\t\tuntilRes =  new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", c.getTime().format(\"yyyy-MM-dd'T'00:00:00XX\", location.timeZone))\n\t}\n\telse if (until ==~ /\\d+.*T.*/) { // until is a ISO-8601 date string, so parse:\n\t\tuntilRes = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", until)\n\t}\n\telse if (until.isNumber()) { // until is a duration in minutes, so construct date from now():\n\t\t// Evohome supposedly only accepts setpoints for up to 24 hours, so we should limit minutes to 1440.\n\t\t// For now, just pass any duration and see if Evohome accepts it...\n\t\tuntilRes = new Date( now() + (Math.round(until) * 60000) )\n\t}\n\telse {\n\t\tlog.warn \"${device.label}: setHeatingSetpoint(): until value could not be parsed. Setpoint will be applied permanently.\"\n\t\tuntilRes = 0\n\t}\n\t\n\tlog.info \"${device.label}: setHeatingSetpoint(): Setting setpoint to: ${setpoint} until: ${untilRes}\"\n\t\n\t// Send update via parent:\n\tif (!parent.setHeatingSetpoint(state.zoneId, setpoint, untilRes)) {\n\t\t// Command was successful, but it takes a few seconds for the Evohome cloud service to update with new values.\n\t\t// Meanwhile, we know the new setpoint and thermostatSetpointMode anyway:\n\t\tsendEvent(name: 'heatingSetpoint', value: setpoint)\n\t\tsendEvent(name: 'thermostatSetpoint', value: setpoint)\n\t\tsendEvent(name: 'thermostatSetpointMode', value: (0 == untilRes) ? 'permanentOverride' : 'temporaryOverride' )\n\t\tsendEvent(name: 'thermostatSetpointUntil', value: (0 == untilRes) ? null : untilRes.format(\"yyyy-MM-dd'T'HH:mm:00XX\", TimeZone.getTimeZone('UTC')))\n\t\tcalculateThermostatOperatingState()\n\t\tcalculateOptimisations()\n\t\tcalculateThermostatStatus()\n\t\tsendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)\n\t\tpseudoSleep(state.updateRefreshTime * 1000)\n\t\tparent.poll(state.zoneId)\n\t\treturn null\n\t}\n\telse {\n\t\tlog.error \"${device.label}: setHeatingSetpoint(): Error: Unable to set heating setpoint.\"\n\t\treturn 'error'\n\t}\n}\n\n\n\n/**\n *  clearHeatingSetpoint()\n * \n *  Clear the heatingSetpoint. Will return heatingSetpoint to scheduled value.\n *  thermostatSetpointMode should return to \"followSchedule\".\n * \n **/\ndef clearHeatingSetpoint() {\n\n\tlog.info \"${device.label}: clearHeatingSetpoint()\"\n\n\t// Send update via parent:\n\tif (!parent.clearHeatingSetpoint(state.zoneId)) {\n\t\t// Command was successful, but it takes a few seconds for the Evohome cloud service\n\t\t// to update the zone status with the new heatingSetpoint.\n\t\t// Meanwhile, we know the new thermostatSetpointMode is \"followSchedule\".\n\t\tsendEvent(name: 'thermostatSetpointMode', value: 'followSchedule')\n\t\tsendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)\n\t\t// sleep command is not allowed in SmartThings, so we use psuedoSleep().\n\t\tpseudoSleep(state.updateRefreshTime * 1000)\n\t\tparent.poll(state.zoneId)\n\t\treturn null\n\t}\n\telse {\n\t\tlog.error \"${device.label}: clearHeatingSetpoint(): Error: Unable to clear heating setpoint.\"\n\t\treturn 'error'\n\t}\n}\n\n\n/**\n *  raiseSetpoint()\n * \n *  Raise heatingSetpoint and thermostatSetpoint.\n *  Increments by state.temperatureResolution (usually 0.5).\n *\n *  Called by raiseSetpoint tile.\n * \n **/\nvoid raiseSetpoint() {\n\n\tif (state.debug) log.debug \"${device.label}: raiseSetpoint()\"\n\t\n\tdef mode = device.currentValue(\"thermostatMode\")\n\tdef targetSp = new BigDecimal(state.targetSetpoint)\n\tdef tempRes = new BigDecimal(state.temperatureResolution) // (normally 0.5)\n\tdef maxSp = new BigDecimal(state.maxHeatingSetpoint)\n\t\n\tif ('off' == mode || 'away' == mode) {\n\t\tlog.warn \"${device.label}: raiseSetpoint(): thermostat mode (${mode}) does not allow altering the temperature setpoint.\"\n\t}\n\telse {\n\t\ttargetSp += tempRes\n\n\t\tif (targetSp > maxSp) {\n\t\t\ttargetSp = maxSp\n\t\t}\n\t\t\n\t\tstate.targetSetpoint = targetSp\n\t\tlog.info \"${device.label}: raiseSetpoint(): Target setpoint raised to: ${targetSp}\"\n\t\tsendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)\n\t\trunIn(3, \"alterSetpoint\", [overwrite: true]) // Wait three seconds in case targetSetpoint is changed again.\n\t}\n\t\n}\n\n\n/**\n *  lowerSetpoint()\n * \n *  Lower heatingSetpoint and thermostatSetpoint.\n *  Increments by state.temperatureResolution (usually 0.5).\n *\n *  Called by lowerSetpoint tile.\n * \n **/\nvoid lowerSetpoint() {\n\n\tif (state.debug) log.debug \"${device.label}: lowerSetpoint()\"\n\t\n\tdef mode = device.currentValue(\"thermostatMode\")\n\tdef targetSp = new BigDecimal(state.targetSetpoint)\n\tdef tempRes = new BigDecimal(state.temperatureResolution) // (normally 0.5)\n\tdef minSp = new BigDecimal(state.minHeatingSetpoint)\n\t\n\tif ('off' == mode || 'away' == mode) {\n\t\tlog.warn \"${device.label}: lowerSetpoint(): thermostat mode (${mode}) does not allow altering the temperature setpoint.\"\n\t}\n\telse {\n\t\ttargetSp -= tempRes \n\n\t\tif (targetSp < minSp) {\n\t\t\ttargetSp = minSp\n\t\t}\n\t\t\n\t\tstate.targetSetpoint = targetSp\n\t\tlog.info \"${device.label}: lowerSetpoint(): Target setpoint lowered to: ${targetSp}\"\n\t\tsendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)\n\t\trunIn(3, \"alterSetpoint\", [overwrite: true]) // Wait three seconds in case targetSetpoint is changed again.\n\t}\n\t\n}\n\n\n/**\n *  alterSetpoint()\n * \n *  Proxy command called by raiseSetpoint and lowerSetpoint, as runIn \n *  cannot pass targetSetpoint diretly to setHeatingSetpoint.\n *\n **/\nprivate alterSetpoint() {\n\n\tif (state.debug) log.debug \"${device.label}: alterSetpoint()\"\n\t\n\tsetHeatingSetpoint(state.targetSetpoint)\n}\n\n\n/**********************************************************************\n *  Convenience Commands:\n *   These commands alias other commands with preset parameters.\n **********************************************************************/\n\nvoid resume() {\n\tif (state.debug) log.debug \"${device.label}: resume()\"\n\tclearHeatingSetpoint()\n}\n\nvoid auto() {\n\tif (state.debug) log.debug \"${device.label}: auto()\"\n\tsetThermostatMode('auto')\n}\n\nvoid heat() {\n\tif (state.debug) log.debug \"${device.label}: heat()\"\n\tsetThermostatMode('auto')\n}\n\nvoid off() {\n\tif (state.debug) log.debug \"${device.label}: off()\"\n\tsetThermostatMode('off')\n}\n\nvoid away(until=-1) {\n\tif (state.debug) log.debug \"${device.label}: away()\"\n\tsetThermostatMode('away', until)\n}\n\nvoid custom(until=-1) {\n\tif (state.debug) log.debug \"${device.label}: custom()\"\n\tsetThermostatMode('custom', until)\n}\n\nvoid dayOff(until=-1) {\n\tif (state.debug) log.debug \"${device.label}: dayOff()\"\n\tsetThermostatMode('dayOff', until)\n}\n\nvoid economy(until=-1) {\n\tif (state.debug) log.debug \"${device.label}: economy()\"\n\tsetThermostatMode('economy', until)\n}\n\nvoid boost() {\n\tif (state.debug) log.debug \"${device.label}: boost()\"\n\tsetHeatingSetpoint(state.boostTemperature)\n}\n\nvoid suppress() {\n\tif (state.debug) log.debug \"${device.label}: suppress()\"\n\tsetHeatingSetpoint(state.suppressTemperature)\n}\n\n/**********************************************************************\n *  Helper Commands:\n **********************************************************************/\n\n/**\n *  pseudoSleep(ms)\n * \n *  Substitute for sleep() command.\n *\n **/\nprivate pseudoSleep(ms) {\n\tdef start = now()\n\twhile (now() < start + ms) {\n\t\t// Do nothing, just wait.\n\t}\n}\n\n\n/**\n *  getInputDefaultValue(inputName)\n * \n *  Get the default value for the specified input.\n *\n **/\nprivate getInputDefaultValue(inputName) {\n\n\tif (state.debug) log.debug \"${device.label}: getInputDefaultValue()\"\n\t\n\tdef returnValue\n\tproperties.preferences?.sections.each { section ->\n\t\tsection.input.each { input ->\n\t\t\tif (input.name == inputName) {\n\t\t\t\treturnValue = input.defaultValue\n\t\t\t}\n\t\t}\n\t}\n\t\n\treturn returnValue\n}\n\n\n\n/**\n *  formatTemperature(t)\n * \n *  Format temperature value to one decimal place.\n *  t:   can be string, float, bigdecimal...\n *  Returns as string.\n **/\nprivate formatTemperature(t) {\n\t//return Float.parseFloat(\"${t}\").round(1)\n\t//return String.format(\"%.1f\", Float.parseFloat(\"${t}\").round(1))\n\treturn Float.parseFloat(\"${t}\").round(1).toString()\n}\n\n\n/**\n *  formatThermostatModeForDisp(mode)\n * \n *  Translate SmartThings values to display values.\n *   \n **/\nprivate formatThermostatModeForDisp(mode) {\n\n\tif (state.debug) log.debug \"${device.label}: formatThermostatModeForDisp()\"\n\n\tswitch (mode) {\n\t\tcase 'auto':\n\t\t\tmode = 'Auto'\n\t\t\tbreak\n\t\tcase 'economy':\n\t\t\tmode = 'Economy'\n\t\t\tbreak\n\t\tcase 'away':\n\t\t\tmode = 'Away'\n\t\t\tbreak\n\t\tcase 'custom':\n\t\t\tmode = 'Custom'\n\t\t\tbreak\n\t\tcase 'dayOff':\n\t\t\tmode = 'Day Off'\n\t\t\tbreak\n\t\tcase 'off':\n\t\t\tmode = 'Off'\n\t\t\tbreak\n\t\tdefault:\n\t\t\tmode = 'Unknown'\n\t\t\tbreak\n\t}\n\n\treturn mode\n }\n  \n\n/**\n *  calculateThermostatOperatingState()\n * \n *  Calculates thermostatOperatingState and generates event accordingly.\n *\n **/\nprivate calculateThermostatOperatingState() {\n\n\tif (state.debug) log.debug \"${device.label}: calculateThermostatOperatingState()\"\n\n\tdef tOS\n\tif ('off' == device.currentValue('thermostatMode')) {\n\t\ttOS = 'off'\n\t}\n\telse if (device.currentValue(\"temperature\") < device.currentValue(\"thermostatSetpoint\")) {\n\t\ttOS = 'heating'\n\t}\n\telse {\n\t\ttOS = 'idle'\n\t}\n\t\n\tsendEvent(name: 'thermostatOperatingState', value: tOS)\n}\n\n\n/**\n *  calculateOptimisations()\n * \n *  Calculates if optimisation and windowFunction are active \n *  and generates events accordingly.\n *\n *  This isn't going to be 100% perfect, but is reasonably accurate.\n *\n **/\nprivate calculateOptimisations() {\n\n\tif (state.debug) log.debug \"${device.label}: calculateOptimisations()\"\n\n\tdef newOptValue = 'inactive'\n\tdef newWdfValue = 'inactive'\n\t\n    // Convert temp values to BigDecimals for comparison:\n\tdef heatingSp = new BigDecimal(device.currentValue('heatingSetpoint'))\n\tdef scheduledSp = new BigDecimal(device.currentValue('scheduledSetpoint'))\n\tdef nextScheduledSp = new BigDecimal(device.currentValue('nextScheduledSetpoint'))\n\tdef windowTemp = new BigDecimal(state.windowFunctionTemperature)\n    \n\tif ('auto' != device.currentValue('thermostatMode')) {\n\t\t// Optimisations cannot be active if thermostatMode is not 'auto'.\n\t}\n\telse if ('followSchedule' != device.currentValue('thermostatSetpointMode')) {\n\t\t// Optimisations cannot be active if thermostatSetpointMode is not 'followSchedule'.\n\t\t// There must be a manual override.\n\t}\n\telse if (heatingSp == scheduledSp) {\n\t\t// heatingSetpoint is what it should be, so no reason to suspect that optimisations are active.\n\t}\n\telse if (heatingSp == nextScheduledSp) {\n\t\t// heatingSetpoint is the nextScheduledSetpoint, so optimisation is likely active:\n\t\tnewOptValue = 'active'\n\t}\n\telse if (heatingSp == windowTemp) {\n\t\t// heatingSetpoint is the windowFunctionTemp, so windowFunction is likely active:\n\t\tnewWdfValue = 'active'\n\t}\n   \n\tsendEvent(name: 'optimisation', value: newOptValue)\n\tsendEvent(name: 'windowFunction', value: newWdfValue)\n\n}\n\n\n/**\n *  calculateThermostatStatus()\n * \n *  Calculates thermostatStatus and generates event accordingly.\n *\n *  thermostatStatus is a text summary of thermostatMode and thermostatOperatingState.\n *\n **/\nprivate calculateThermostatStatus() {\n\n\tif (state.debug) log.debug \"${device.label}: calculateThermostatStatus()\"\n\n\tdef newThermostatStatus = ''\n\tdef thermostatModeDisp = formatThermostatModeForDisp(device.currentValue('thermostatMode'))\n\tdef setpoint = device.currentValue('thermostatSetpoint')\n\t\n\tif ('Off' == thermostatModeDisp) {\n\t\tnewThermostatStatus = 'Off'\n\t}\n\telse if('heating' == device.currentValue('thermostatOperatingState')) {\n\t\tnewThermostatStatus = \"Heating to ${setpoint} (${thermostatModeDisp})\"\n\t}\n\telse {\n\t\tnewThermostatStatus = \"Idle (${thermostatModeDisp})\"\n\t}\n\t\n\tsendEvent(name: 'thermostatStatus', value: newThermostatStatus)\n}\n\n\n\n/**\n *  calculateThermostatSetpointStatus()\n * \n *  Calculates thermostatSetpointStatus and generates event accordingly.\n *\n *  thermostatSetpointStatus is a text summary of thermostatSetpointMode and thermostatSetpointUntil. \n *  It also indicates if 'optimisation' or 'windowFunction' is active.\n *\n **/\nprivate calculateThermostatSetpointStatus() {\n\n\tif (state.debug) log.debug \"${device.label}: calculateThermostatSetpointStatus()\"\n\n\tdef newThermostatSetpointStatus = ''\n\tdef setpointMode = device.currentValue('thermostatSetpointMode')\n\t\n\tif ('off' == device.currentValue('thermostatMode')) {\n\t\tnewThermostatSetpointStatus = 'Off'\n\t}\n\telse if ('away' == device.currentValue('thermostatMode')) {\n\t\tnewThermostatSetpointStatus = 'Away'\n\t}\n\telse if ('active' == device.currentValue('optimisation')) {\n\t\tnewThermostatSetpointStatus = 'Optimisation Active'\n\t}\n\telse if ('active' == device.currentValue('windowFunction')) {\n\t\tnewThermostatSetpointStatus = 'Window Function Active'\n\t}\n\telse if ('followSchedule' == setpointMode) {\n\t\tnewThermostatSetpointStatus = 'Following Schedule'\n\t}\n\telse if ('permanentOverride' == setpointMode) {\n\t\tnewThermostatSetpointStatus = 'Permanent'\n\t}\n\telse {\n\t\tdef untilStr = device.currentValue('thermostatSetpointUntil')\n\t\tif (untilStr) {\n\t\t\n\t\t\t//def nowDate = new Date()\n\t\t\t\n\t\t\t// thermostatSetpointUntil is an ISO-8601 date format in UTC, and parse() seems to assume date is in UTC.\n\t\t\tdef untilDate = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", untilStr) \n\t\t\tdef untilDisp = ''\n\t\t\t\n\t\t\tif (untilDate.format(\"u\") == new Date().format(\"u\")) { // Compare day of week to current day of week (today).\n\t\t\t\tuntilDisp = untilDate.format(\"HH:mm\", location.timeZone) // Same day, so just show time.\n\t\t\t}\n\t\t\telse {\n\t\t\t\tuntilDisp = untilDate.format(\"HH:mm 'on' EEEE\", location.timeZone) // Different day, so include name of day.\n\t\t\t}\n\t\t\tnewThermostatSetpointStatus = \"Temporary Until ${untilDisp}\"\n\t\t}\n\t\telse {\n\t\t\tnewThermostatSetpointStatus = \"Temporary\"\n\t\t}\n\t}\n\t\n\tsendEvent(name: 'thermostatSetpointStatus', value: newThermostatSetpointStatus)\n}"
  },
  {
    "path": "devices/fibaro-dimmer-2/README.md",
    "content": "# Fibaro Dimmer 2 (FGD-212)\nhttps://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2\n\nCopyright (c) [David Lomas](https://github.com/codersaur)\n\n## Overview\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-on.png\" width=\"200\" align=\"right\">\nAn advanced SmartThings device handler for the Fibaro Dimmer 2 (FGD-212) Z-Wave Dimmer.\n\n### Key features:\n* Z-Wave parameters can be configured using the SmartThings GUI.\n* Multi-channel device associations can be configured using the SmartThings GUI.\n* Child protection modes can be configured using the SmartThings GUI.\n* _Fault_ tile indicates burnt-out bulb / overload / hardware errors.\n* _Scene_ tile indicates last activated scene.\n* _Sync_ tile indicates when all configuration options are successfully synchronised with the physical device.\n* Dimmer _level_ range is now 0-100% (instead of 0-99%).\n* _Nightmode_ feature allows switch-on brightness level to be controlled on a schedule.\n* Logger functionality enables critical errors and warnings to be saved to the _logMessage_ attribute.\n* Extensive inline code comments to support community development.\n\n## Installation\n\n1. Follow [these instructions](https://github.com/codersaur/SmartThings#device-handler-installation-procedure) to install the device handler in the SmartThings IDE.\n\n2. **Note for iPhone users**: The _defaultValue_ of inputs (preferences) are commented out to cater for Android users. iPhone users can uncomment these lines if they wish (search for \"iPhone\" in the code).\n\n3. From the SmartThings app on your phone, edit the device settings to suit your installation and hit _Done_. The first configuration sync may take some time. If the device has not synced after 2 minutes, tap the _sync_ tile to force any remaining configuration items to be synchronised.\n**Note, if you are upgrading from an earlier version of this device handler it is still important to review all the settings, as many will not carry over from earlier versions!**\n\n## Settings\n\n#### General Settings:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-general.png\" width=\"200\" align=\"right\">\n\n* **IDE Live Logging Level**: Set the level of log messages shown in the SmartThings IDE _Live Logging_ tab. For normal operation _Info_ or _Warning_ is recommended, if troubleshooting use _Debug_ or _Trace_.\n\n* **Device Logging Level**: Set the level of log messages that will be recorded in the device's _logMessage_ attribute. This offers a way to review historical messages without having to keep the IDE _Live Logging_ screen open. To prevent excessive events, the maximum level supported is _Warning_.\n\n* **Force Full Sync**: By default, only settings that have been modified will be synchronised with the device. Enable this setting to force all device parameters, association groups, and protection settings to re-sent to the device. This will take several minutes and you may need to press the _sync_ tile a few times before the device is fully synced.\n\n* **Proactively Request Reports**: If you find that device status is slow to update, enabling this setting will cause additional reports to be requested.\n\n\n#### Child Protection Mode:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-protection.png\" width=\"200\" align=\"right\">\n\nThe Fibaro Dimmer 2 supports the Z-wave Protection Command Class. This allows the device to be protected from unintentional control (e.g. by a child) by disabling the physical switches and/or RF control.\n\n* **Local Protection**: Setting this option to _No operation possible_ will disable both physical switches (S1/S2).\n\n* **RF Protection**: Setting this option to _No RF control_ will prevent z-wave commands from altering the device state. This includes commands received from the hub as well as other associated devices.\n\n\n#### Nightmode:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-nightmode.png\" width=\"200\" align=\"right\">\n\nThe _Nightmode_ feature forces the dimmer to switch on at a specified level. (Behind the scenes, this feature is updating Paramter #19). _Nightmode_ can be manually enabled/disabled using the _Nightmode_ tile and can also be scheduled using the settings here.\n\n* **Nightmode Level**: The dimmer will always switch on at this level when _Nightmode_ is enabled.\n\n* **Force Nightmode**: If the dimmer is on when _Nightmode_ is enabled, the _Nightmode Level_ is applied immediately (otherwise it's only applied next time the dimmer is switched on). Similarly, if the dimmer is on when _Nightmode_ is disabled, the brightness level will immediately be returned to the state prior to _Nightmode_ being enabled.\n\n* **Nightmode Start Time**: _Nightmode_ will be enabled every day at this time.\n\n* **Nightmode Stop Time**: _Nightmode_ will be disabled every day at this time.\n\nIf _Nightmode_ Start and Stop times are set here, they will only apply to the corresponding instance of the device. If you want to implement a _Nightmode_ schedule for multiple devices it is possible to write a simple SmartApp (or use CoRE) to  call the _enableNightmode()_ and _disableNightmode()_ commands on each device.\n\n#### Device Parameters:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-params.png\" width=\"200\" align=\"right\">\n\nThe settings in this section can be used to specify the value of all writable device parameters. It is recommended to consult the [manufacturer's manual](http://manuals.fibaro.com/dimmer-2/) for a full description of each parameter.\n\nIf no value is specified for a parameter, then it will not be synched with the device and the existing value in the device will be preserved.\n\n##### Auto-calibration:\nIf parameter #13 is used to force auto-calibration of the device, any values that are specified for parameters #1, #2, and #30 will be ignored. After hitting _Done_, monitor the _Live Logging_ tab in the IDE to discover what the new auto-calibrated values are (the auto-calibrated values will not be updated in the device's settings screen due to limitations of the SmartThings platform).\n\nNext time device settings are updated, remember to set parameter #13 back to _0: Readout_ if you do not want auto-calibration to be forced again. Additionally, review parameters #1, #2, and #30, as any values specified will over-write the auto-calibrated values.\n\n##### Read-only Parameters:\nThe Fibaro Dimmer 2 has a few read-only parameters that are not shown in this section. The dimmer will periodically report the values of these read-only parameters to the hub, and their values can be seen in the _Live Logging_ tab in the IDE.\n\n#### Multi-channel Device Associations:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-assocgroups.png\" width=\"200\" align=\"right\">\n\nThe Fibaro Dimmer 2 supports _Multi-channel_ Device Associations. This allows the physical switches connected to a Fibaro Dimmer 2 to send z-wave commands directly to groups of other devices (e.g. other dimmers or relays), without the commands being processed by the SmartThings hub. This results in faster response times compared to using a SmartApp for example.\n\nThe Fibaro Dimmer 2 supports four association groups:\n\n- **Association Group #2**: Sends on/off commands (BASIC_SET) when Switch #1 (S1) is used.\n- **Association Group #3**: Sends dim/brighten commands (SWITCH_MUTLILEVEL_SET) when Switch #1 (S1) is used.\n- **Association Group #4**: Sends on/off commands (BASIC_SET) when Switch #2 (S2) is used.\n- **Association Group #5**: Sends dim/brighten commands (SWITCH_MUTLILEVEL_SET) when Switch #2 (S2) is used.\n\nThe members of each _Association Group_ must be defined as a comma-delimited list of target nodes. Each target device can be specified in one of two ways:\n- _Node_: A single hexadecimal number (e.g. \"0C\") representing the target _Device Network ID_.\n- _Endpoint_: A pair of hexadecimal numbers separated by a colon (e.g. \"10:1\") that represent the target _Device Network ID_ and _Endpoint ID_ respectively. For devices that support multiple endpoints, this allows a specific endpoint to be targeted by the association group.\n\nYou can find the _Device Network ID_ for all Z-Wave devices in your SmartThings network from the _My Devices_ tab in the SmartThings IDE. Consult the relevant manufacturer's manual for information about the endpoints supported by a particular target device.\n\n## GUI\n\n#### Power and Energy Tiles:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-power-energy.png\" width=\"200\" align=\"right\">\n\nThese tiles display the instantaneous power consumption of the device (Watts) and the accumulated energy consumption (KWh). The _Now:_ tile can be tapped to force the device state to be refreshed. The _Since: ..._ tile can be tapped to reset the _Accumulated Energy_ figure.\n\n\n#### Nightmode Tile:\nThis tile can be used to toggle (enable/disable) _Nightmode_.\n\n#### Scene Tile:\nIf parameter #28 (Scene Activation) has been enabled, then the Fibaro Dimmer 2 will send SCENE_ACTIVATION_SET commands to the SmartThings hub. This tile will indicate the ID of the last-activated scene.\n\n#### Sync Tile:\nThis tile indicates when all configuation settings have been successfully synchronised with the physical device. If the tile remains in the orange SYNC PENDING state, tap it to force any remaining unsynced items to be sent to the device.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-sync.png\" width=\"200\">\n\n#### Fault Tile:\nThis tile indicates if the device has reported any faults. These may include burnt-out-bulbs (load error), overload, low voltage, surge, temperature warnings, firmware, or hardware issues. Once any faults have been investigated and remediated, the tile can be tapped to clear the fault status.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-fault.png\" width=\"200\">\n\n## SmartApp Integration\n\n#### Attributes:\n\nThe device handler has the following attributes:\n\n* **switch [ENUM]**: The switch State, 'On' or 'Off'.\n* **level [NUMBER]**: The current light level (0-100%).\n* **power [NUMBER]**: The current instantaneous power usage (Watts).\n* **energy [NUMBER]**: The Accumulated energy consumption (KWh).\n* **energyLastReset**: Last time that the _Accumulated Energy_ figure was reset.\n* **scene [NUMBER]**: ID of last-activated scene.\n* **nightmode [ENUM]**: Indicates if _Nightmode_ is 'Enabled' or 'Disabled'.\n* **syncPending [NUMBER]**: The number of configuration items that need to be synced with the physical device. _0_ if the device is fully synchronised.\n* **fault [ENUM]**: Indicates if the device has any faults. '_clear_' if there are no active faults.\n* **logMessage [STRING]**: Important log messages.\n\n#### Commands:\n\nThe device exposes the following custom commands which can be called from a SmartApp:\n\n* **enableNightmode(level)**: Enable _Nightmode_. The optional level parameter will override the _Nightmode Level_.\n* **disableNightmode()**: Disable _Nightmode_.\n* **toggleNightmode()**: Toggle _Nightmode_.\n* **clearFault()**: Clear any active faults.\n* **reset()**: Alias for _resetEnergy()_.\n* **resetEnergy()**: Reset the _Accumulated Energy_ figure back to _0_.\n* **sync()**: Trigger device synchronisation.\n\n## Version History\n\n#### 2017-02-27: v2.02:\n * Fixed backgroundColor for fault tile.\n\n#### 2017-02-25: v2.01:\n  * Preferences: defaultValues are commented out by default to cater for Android users. iPhone users can uncomment these lines if they wish (search for \"iPhone\").\n  * updated(): Fix to allow device to sync after a forced auto-calibration.\n  * updateSyncPending(): If a target value is null, then it does not need syncing.\n\n#### 2017-02-24: v2.00\n * Complete re-write in-line with new coding standards.\n * General Behaviour Changes:\n  *     Dimmer level now reverts to zero when switched off.\n  *     Dimmer level range is now 0-100%.\n  *     Fewer report requests are made, as the Fibaro Dimmer 2 is good at sending back reports anyway.\n  *     Nightmode scheduling fixed after change in the behaviour of _schedule()_.\n *   Capabilities:\n  *     Added \"Light\" capability.\n  *     Added unofficial \"Fault\" capability. [attributes: 'fault', commands: clearFault()]\n  *     Added unofficial \"Logging\" capability. [attributes: 'logMessage']\n  *     Added unofficial \"Scene Controller\" capability. [attributes: 'scene']\n  *     Removed \"Configuration\" capability and configure() command, as not used.\n *   Attributes:\n  *     energyLastReset: renamed from lastReset.\n  *     logMessage: Critical error and warning log messages.\n  *     syncPending: Number of items that need to be synced with the physical device.\n  *     fault: Indicates if the device has any faults (load, surge, overload, overCurrent, voltage, temperature, hardware, firmware). 'clear' if no active faults.\n *   Commands:\n  *     resetEnergy(): Resets accumulated energy figure.\n  *     clearFault(): Clears any active faults.\n *   Fingerprints: Updated to use new Z-Wave fingerprint format.\n *   Tiles:\n  *     level: range is now 0-100%\n  *     scene: Indicates last activated scene.\n  *     syncPending: Shows when device configuration is synced.\n  *     fault: Indicates device faults.\n *   Settings/Preferences:\n  *     Proactive Requests.\n  *     IDE Live Logging Level\n  *     Device Logging Level\n  *     Association Group members can be configured from the Settings GUI, including multi-channel endpoint destinations.\n  *     Protection Options can be set for local (physical switches) and RF Control, to prevent unintentional changes.\n *   zwaveEvent():\n  *     zwaveEvent(CONFIGURATION_REPORT): Uses new scaledConfigurationValue attribute.\n  *     zwaveEvent(POWERLEVEL_REPORT): New handler for powerlevel reports.\n  *     zwaveEvent(COMMAND_CLASS_SWITCH_BINARY): Removed as it doesn't appear to be supported by the device.\n  *     zwaveEvent(ASSOCIATION_REPORT): New handlers for both normal and multi-channel association reports.\n  *   dimmerEvent(): Various optimisations and fixes.\n *   update():\n  *     Added a check to prevent double execution.\n  *     Requests Firmware Metadata, Manufacturer-specific, and Version reports.\n *   New custom commands:\n  *     clearFault(): Clears any active fault.\n  *     resetEnergy(): Reset the Accumulated Energy figure in the device.\n *   New private helper functions:\n  *     logger(): Wrapper function for all logging: Logs events to IDE Live Logging, and also by raising logMessage events. Configured using configLoggingLevelIDE and configLoggingLevelDevice preferences.\n  *     sync(): Manages synchronisation of all parameters and association groups with the physical device. The syncPending attribute advertises remaining number of sync operations.\n  *     refreshConfig(): Requests all configuration, association group reports.\n  *     sendSecureSequence(): Secure an array of commands and send them using sendHubCommand.\n  *     Additional functions to dynamically build the parameters and association groups preferences.\n *   New Metadata Funtions:\n  *     getCommandClassVersions(): Returns supported command class versions.\n  *     getParamsMd(): Returns device parameters metadata (including read-only parameters).\n  *     getAssocGroupsMd(): Returns association groups metadata.\n\n#### 2016-10-31: v1.03\n  *  Added event handlers for Crc16Encap, SensorMultilevelReport, ManufacturerSpecificReport, VersionReport, and FirmwareMdReport.\n\n#### 2016-10-24: v1.02\n  *  Increased delay between ConfigurationSet commands to 500ms to improve reliability of sending parameters.\n\n#### 2016-10-11: v1.01\n  *   Added Nightmode functionality.\n  *   dimmerEvents(): Fixed MeterGet requests after a switch or level state change.\n  *   on(), off(), setLevel(): Delayed switchMultilevelGet() requests by 8s as early requests generate erroneous data. The dimmer will send a correct report once it has completed the request anyway (which is usually sooner than 8s).\n  *   zwaveEvent(_MeterReport_): Removed rounding of power values. Also, Energy and power values are stored as dispEnergy and dispPower to work-around UI formatting issue.\n  *   Settings/Preferences: Fixed param24/25/27 to allow combination of options.\n  *   Simplified fingerprint.\n\n#### 2016-10-05: v1.00\n  *  Initial version based on device handler by hajar97.\n  *  Tiles: Added GetConfig button to retrieve the current device settings (which are displayed in the debug log).\n\n## To Do\n *   Optimise zwaveEvent(CRC_16_ENCAP) by using _ecapsulatedCommand()_, once implemenation has been fixed by SmartThings.\n *   Allow protection state to be controlled via commands (maybe just the local). This would allow a smartApp to disable all physical light switches, perhaps on a schedule, for example. E.g. stop children turning on the lights after 10PM. Similar to Nightmode.\n *   Add _Button_ capability and raise _button_ events.\n\n## Physical Device Notes\n\nGeneral notes relating to the Fibaro Dimmer 2:\n\n* The device has three read-only parameters. These are not shown in the settings GUI, but their values will be reported in the Live Logging tab of the IDE when Configuration Reports are received.\n\n## References\n Some useful links relevant to the development of this device handler:\n* [Fibaro Dimmer 2 - Z-Wave certification information](http://products.z-wavealliance.org/products/1729)\n* [Fibaro Dimmer 2 - Manual](http://manuals.fibaro.com/dimmer-2/)\n\n## License\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n"
  },
  {
    "path": "devices/fibaro-dimmer-2/fibaro-dimmer-2.groovy",
    "content": "/*****************************************************************************************************************\n *  Copyright: David Lomas (codersaur)\n *\n *  Name: Fibaro Dimmer 2\n *\n *  Author: David Lomas (codersaur)\n *\n *  Date: 2017-02-27\n *\n *  Version: 2.02\n *\n *  Source: https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2\n *\n *  Author: David Lomas (codersaur)\n *\n *  Description: An advanced SmartThings device handler for the Fibaro Dimmer 2 (FGD-212) Z-Wave Dimmer.\n *\n *  For full information, including installation instructions, exmples, and version history, see:\n *   https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n *****************************************************************************************************************/\nmetadata {\n    definition (name: \"Fibaro Dimmer 2\", namespace: \"codersaur\", author: \"David Lomas\") {\n        capability \"Actuator\"\n        capability \"Switch\"\n        capability \"Switch Level\"\n        capability \"Light\"\n        capability \"Sensor\"\n        capability \"Power Meter\"\n        capability \"Energy Meter\"\n        capability \"Polling\"\n        capability \"Refresh\"\n\n        // Custom (Virtual) Capabilities:\n        //capability \"Fault\"\n        //capability \"Logging\"\n        //capability \"Scene Controller\"\n\n        // Standard (Capability) Attributes:\n        attribute \"switch\", \"string\"\n        attribute \"level\", \"number\"\n        attribute \"power\", \"number\"\n        attribute \"energy\", \"number\"\n\n        // Custom Attributes:\n        attribute \"fault\", \"string\"             // Indicates if the device has any faults. 'clear' if no active faults.\n        attribute \"logMessage\", \"string\"        // Important log messages.\n        attribute \"energyLastReset\", \"string\"   // Last time that Accumulated Engergy was reset.\n        attribute \"syncPending\", \"number\"       // Number of config items that need to be synced with the physical device.\n        attribute \"nightmode\", \"string\"         // 'Enabled' or 'Disabled'.\n        attribute \"scene\", \"number\"             // ID of last-activated scene.\n\n        // Display Attributes:\n        // These are only required because the UI lacks number formatting and strips leading zeros.\n        attribute \"dispPower\", \"string\"\n        attribute \"dispEnergy\", \"string\"\n\n        // Custom Commands:\n        command \"reset\"\n        command \"resetEnergy\"\n        command \"enableNightmode\"\n        command \"disableNightmode\"\n        command \"toggleNightmode\"\n        command \"clearFault\"\n        command \"sync\"\n        command \"test\"\n\n        // Fingerprints (new format):\n        fingerprint mfr: \"010F\", prod: \"0102\", model: \"1000\"\n        fingerprint type: \"1101\", mfr: \"010F\", cc: \"5E,86,72,59,73,22,31,32,71,56,98,7A\"\n        fingerprint type: \"1101\", mfr: \"010F\", cc: \"5E,86,72,59,73,22,31,32,71,56,98,7A\", sec: \"20,5A,85,26,8E,60,70,75,27\", secOut: \"2B\"\n    }\n\n    tiles(scale: 2) {\n\n        // Multi Tile:\n        multiAttributeTile(name:\"switch\", type: \"lighting\", width: 6, height: 4, canChangeIcon: true){\n            tileAttribute (\"device.switch\", key: \"PRIMARY_CONTROL\") {\n                attributeState \"on\", label:'${name}', action:\"switch.off\", icon:\"st.switches.switch.on\", backgroundColor:\"#79b821\", nextState:\"turningOff\"\n                attributeState \"off\", label:'${name}', action:\"switch.on\", icon:\"st.switches.switch.off\", backgroundColor:\"#ffffff\", nextState:\"turningOn\"\n                attributeState \"turningOn\", label:'${name}', action:\"switch.off\", icon:\"st.switches.switch.on\", backgroundColor:\"#79b821\", nextState:\"turningOff\"\n                attributeState \"turningOff\", label:'${name}', action:\"switch.on\", icon:\"st.switches.switch.off\", backgroundColor:\"#ffffff\", nextState:\"turningOn\"\n            }\n            tileAttribute (\"device.level\", key: \"SLIDER_CONTROL\", range:\"(0..100)\") {\n                attributeState \"level\", action:\"setLevel\"\n            }\n        }\n\n        // Instantaneous Power:\n        valueTile(\"instMode\", \"device.dispPower\", decoration: \"flat\", width: 2, height: 1) {\n            state \"default\", label:'Now:', action:\"refresh\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_refresh.png\"\n        }\n        valueTile(\"power\", \"device.dispPower\", decoration: \"flat\", width: 2, height: 1) {\n            state \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n        }\n\n        // Accumulated Energy:\n        valueTile(\"energyLastReset\", \"device.energyLastReset\", decoration: \"flat\", width: 2, height: 1) {\n            state \"default\", label:'Since:  ${currentValue}', action:\"resetEnergy\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_stopwatch_reset.png\"\n        }\n        valueTile(\"energy\", \"device.dispEnergy\", width: 2, height: 1) {\n            state \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n        }\n\n        // Other Tiles:\n        standardTile(\"nightmode\", \"device.nightmode\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'${currentValue}', action:\"toggleNightmode\", icon:\"st.Weather.weather4\"\n        }\n        valueTile(\"scene\", \"device.scene\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Scene: ${currentValue}'\n        }\n        standardTile(\"refresh\", \"device.power\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'', action:\"refresh\", icon:\"st.secondary.refresh\"\n        }\n        standardTile(\"syncPending\", \"device.syncPending\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Sync Pending', action:\"sync\", backgroundColor:\"#FF6600\"\n            state \"0\", label:'Synced', action:\"\", backgroundColor:\"#79b821\"\n        }\n        standardTile(\"fault\", \"device.fault\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'${currentValue} Fault', action:\"clearFault\", backgroundColor:\"#FF6600\", icon:\"st.secondary.tools\"\n            state \"clear\", label:'${currentValue}', action:\"\", backgroundColor:\"#79b821\", icon:\"\"\n        }\n        standardTile(\"test\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Test', action:\"test\"\n        }\n\n        // Tile Layouts:\n        main([\"switch\"])\n        details([\n            \"switch\",\n            \"instMode\",\"power\",\n            \"nightmode\",\n            \"energyLastReset\",\"energy\",\n            \"scene\",\n            //\"refresh\",\n            //\"test\",\n            \"syncPending\",\n            \"fault\"\n        ])\n    }\n\n    preferences {\n\n        section { // GENERAL:\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"GENERAL:\",\n                description: \"General device handler settings.\"\n            )\n\n            input (\n                name: \"configLoggingLevelIDE\",\n                title: \"IDE Live Logging Level: Messages with this level and higher will be logged to the IDE.\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"None\",\n                    \"1\" : \"Error\",\n                    \"2\" : \"Warning\",\n                    \"3\" : \"Info\",\n                    \"4\" : \"Debug\",\n                    \"5\" : \"Trace\"\n                ],\n//                defaultValue: \"3\", // iPhone users can uncomment these lines!\n                required: true\n            )\n\n            input (\n                name: \"configLoggingLevelDevice\",\n                title: \"Device Logging Level: Messages with this level and higher will be logged to the logMessage attribute.\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"None\",\n                    \"1\" : \"Error\",\n                    \"2\" : \"Warning\"\n                ],\n//                defaultValue: \"2\", // iPhone users can uncomment these lines!\n                required: true\n            )\n\n            input (\n                name: \"configSyncAll\",\n                title: \"Force Full Sync: All device parameters, association groups, and protection settings will \" +\n                \"be re-sent to the device. This will take several minutes and you may need to press the 'sync' \" +\n                \"tile a few times.\",\n                type: \"boolean\",\n//                defaultValue: false, // iPhone users can uncomment these lines!\n                required: true\n            )\n\n            input (\n                name: \"configProactiveReports\",\n                title: \"Proactively Request Reports: Additonal requests for status reports will be made. \" +\n                \"Use only if status reporting is unreliable.\",\n                type: \"boolean\",\n//                defaultValue: false, // iPhone users can uncomment these lines!\n                required: true\n            )\n        }\n\n        section { // PROTECTION:\n            input type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"PROTECTION:\",\n                description: \"Prevent unintentional control (e.g. by a child) by disabling the physical switches and/or RF control.\"\n\n            input (\n                name: \"configProtectLocal\",\n                title: \"Local Protection: Applies to physical switches:\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"Unprotected\",\n                    //\"1\" : \"Protection by sequence\", // Not supported by Fibaro Dimmer 2.\n                    \"2\" : \"No operation possible\"\n                ],\n//                defaultValue: \"0\", // iPhone users can uncomment these lines!\n                required: true\n            )\n\n            input (\n                name: \"configProtectRF\",\n                title: \"RF Protection: Applies to Z-Wave commands sent from hub or other devices:\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"Unprotected\",\n                    \"1\" : \"No RF control\"//,\n                    //\"2\" : \"No RF response\" // Not supported by Fibaro Dimmer 2.\n                ],\n//                defaultValue: \"0\", // iPhone users can uncomment these lines!\n                required: true\n            )\n\n        }\n\n        section { // NIGHTMODE:\n            input type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"NIGHTMODE:\",\n                description: \"Nightmode forces the dimmer to switch on at a specific level (e.g. low-level during the night).\\n\" +\n                    \"Nightmode can be enabled/disabled manually using the new Nightmode tile, or scheduled below.\"\n\n            input type: \"number\",\n                name: \"configNightmodeLevel\",\n                title: \"Nightmode Level: The dimmer will always switch on at this level when nightmode is enabled.\",\n                range: \"1..100\",\n//                defaultValue: \"10\", // iPhone users can uncomment these lines!\n                required: true\n\n            input type: \"boolean\",\n                name: \"configNightmodeForce\",\n                title: \"Force Nightmode: If the dimmer is on when nightmode is enabled, the Nightmode Level is applied immediately \" +\n                    \"(otherwise it's only applied next time the dimmer is switched on).\",\n//                defaultValue: true, // iPhone users can uncomment these lines!\n                required: true\n\n            input type: \"time\",\n                name: \"configNightmodeStartTime\",\n                title: \"Nightmode Start Time: Nightmode will be enabled every day at this time.\",\n                required: false\n\n            input type: \"time\",\n                name: \"configNightmodeStopTime\",\n                title: \"Nightmode Stop Time: Nightmode will be disabled every day at this time.\",\n                required: false\n        }\n\n        generatePrefsParams()\n\n        generatePrefsAssocGroups()\n\n    }\n}\n\n/**\n *  parse()\n *\n *  Called when messages from the device are received by the hub. The parse method is responsible for interpreting\n *  those messages and returning event definitions (and command responses).\n *\n *  As this is a Z-wave device, zwave.parse() is used to convert the message into a command. The command is then\n *  passed to zwaveEvent(), which is overloaded for each type of command below.\n *\n *  Parameters:\n *   String      description        The raw message from the device.\n **/\ndef parse(description) {\n    logger(\"parse(): Parsing raw message: ${description}\",\"trace\")\n\n    def result = null\n\n    if (description.startsWith(\"Err\")) {\n        logger(\"parse(): Unknown Error. Raw message: ${description}\",\"error\")\n    }\n    else if (description != \"updated\") {\n        // The purpose of the replace statement here is to fix a bug, see:\n        // https://community.smartthings.com/t/wireless-wall-switch-zme-wallc-s-to-control-smartthings-devices-and-routines/24810/28\n        def cmd = zwave.parse(description.replace(\"98C1\", \"9881\"), getCommandClassVersions())\n        if (cmd) {\n            result = zwaveEvent(cmd)\n        } else {\n            logger(\"parse(): Could not parse raw message: ${description}\",\"error\")\n        }\n    }\n\n    return result\n}\n\n/*****************************************************************************************************************\n *  Z-wave Event Handlers.\n *****************************************************************************************************************/\n\n/**\n *  zwaveEvent( COMMAND_CLASS_BASIC V1 (0x20) : BASIC_REPORT )\n *\n *  The Basic Report command is used to advertise the status of the primary functionality of the device.\n *\n *  Action: Pass command to dimmerEvent().\n *\n *  cmd attributes:\n *    Short    value\n *      0x00       = Off\n *      0x01..0x63 = 0..100%\n *      0xFE       = Unknown\n *      0xFF       = On\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {\n    logger(\"zwaveEvent(): Basic Report received: ${cmd}\",\"trace\")\n    return dimmerEvent(cmd)\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_BASIC V1 (0x20) : BASIC_SET )\n *\n *  The Basic Set command is used to set a value in a supporting device.\n *  If this command is received by the hub, the hub must be a member of one or more association groups.\n *\n *  Action: No action required as state change will be triggered via BASIC_REPORT handler.\n *\n *  cmd attributes:\n *    Short    value\n *      0x00       = Off\n *      0x01..0x63 = 0..100%\n *      0xFE       = Unknown\n *      0xFF       = On\n *\n *  Example: BasicSet(value: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {\n    logger(\"zwaveEvent(): Basic Set received: ${cmd}\",\"trace\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_MULTILEVEL V3 (0x26) : SWITCH_MULTILEVEL_REPORT )\n *\n *  The Switch Multilevel Report is used to advertise the status of a multilevel device.\n *\n *  Action: Pass command to dimmerEvent().\n *\n *  cmd attributes:\n *    Short    value\n *      0x00       = Off\n *      0x01..0x63 = 0..100%\n *      0xFE       = Unknown\n *      0xFF       = On [Deprecated]\n *\n *  Example: SwitchMultilevelReport(value: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) {\n    logger(\"zwaveEvent(): Switch Multilevel Report received: ${cmd}\",\"trace\")\n    return dimmerEvent(cmd)\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_MULTILEVEL V3 (0x26) : SWITCH_MULTILEVEL_SET )\n *\n *  The Switch Multilevel Set command is used to set a value in a supporting device.\n *  If this command is received by the hub, the hub must be a member of one or more association groups.\n *\n *  Action: No action required as state change will be triggered via SWITCH_MULTILEVEL_REPORT handler.\n *\n *  cmd attributes:\n *    Short    value\n *      0x00       = Off\n *      0x01..0x63 = 0..100%\n *      0xFE       = Unknown\n *      0xFF       = On [Deprecated]\n *    Short    dimmingDuration\n *\n *  Example: SwitchMultilevelSet(dimmingDuration: 1, value: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelSet cmd) {\n    logger(\"zwaveEvent(): Switch Multilevel Set received: ${cmd}\",\"trace\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_MULTILEVEL V3 (0x26) : SWITCH_MULTILEVEL_START_LEVEL_CHANGE )\n *\n *  The Multilevel Switch Start Level Change command is used to initiate a transition to a new level.\n *  If this command is received by the hub, the hub must be a member of one or more association groups.\n *\n *  Action: No action required as state change will be triggered via a SWITCH_MULTILEVEL_REPORT on completion\n *  of the transition.\n *\n *  cmd attributes:\n *    Short    dimmingDuration\n *    Boolean  ignoreStartLevel\n *    Short    incDec\n *    Short    startLevel\n *    Short    stepSize\n *    Short    upDown\n *\n *  Example: SwitchMultilevelStartLevelChange(dimmingDuration: 3, ignoreStartLevel: false, incDec: 0,\n *            reserved00: 0, startLevel: 4, stepSize: 1, upDown: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd) {\n    logger(\"zwaveEvent():  Switch Multilevel Start Level Change received: ${cmd}\",\"trace\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_MULTILEVEL V3 (0x26) : SWITCH_MULTILEVEL_STOP_LEVEL_CHANGE )\n *\n *  The Multilevel Switch Stop Level Change command is used to stop an ongoing transition.\n *  If this command is received by the hub, the hub must be a member of one or more association groups.\n *\n *  Action: No action required as state change will be triggered via a SWITCH_MULTILEVEL_REPORT on completion\n *  of the transition.\n *\n *  cmd attributes: None\n *\n *  Example: SwitchMultilevelStopLevelChange()\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) {\n    logger(\"zwaveEvent():  Switch Multilevel Stop Level Change received: ${cmd}\",\"trace\")\n}\n\n/**\n *  dimmerEvent()\n *\n *  Common handler for BasicReport, SwitchBinaryReport, SwitchMultilevelReport.\n *\n *  Action: Raise 'switch' and 'level' events.\n *   Restore pending level if dimmer has been switched on after nightmode has been disabled.\n *   If Proactive Reporting is enabled, and the level has changed, request a meter report.\n **/\ndef dimmerEvent(physicalgraph.zwave.Command cmd) {\n\n    def result = []\n\n    // switch event:\n    def switchValue = (cmd.value ? \"on\" : \"off\")\n    def switchEvent = createEvent(name: \"switch\", value: switchValue)\n    if (switchEvent.isStateChange) logger(\"Dimmer turned ${switchValue}.\",\"info\")\n    result << switchEvent\n\n    // level event:\n    def levelValue = Math.round (cmd.value * 100 / 99)\n    def levelEvent = createEvent(name: \"level\", value: levelValue, unit: \"%\")\n    if (levelEvent.isStateChange) logger(\"Dimmer level is ${levelValue}%\",\"info\")\n    result << levelEvent\n\n    // Store last active level, which is needed for nightmode functionality:\n    if (levelValue > 0) state.lastActiveLevel = levelValue\n\n    // Restore pending level if dimmer has been switched on after nightmode has been disabled:\n    if (!state.nightmodeActive & (state.nightmodePendingLevel > 0) & switchEvent.isStateChange & switchValue == \"on\") {\n        logger(\"dimmerEvent(): Applying Pending Level: ${state.nightmodePendingLevel}\",\"debug\")\n        result << response(secure(zwave.basicV1.basicSet(value: Math.round(state.nightmodePendingLevel.toInteger() * 99 / 100 ))))\n        state.nightmodePendingLevel = 0\n    }\n    // Else if Proactive Reporting is enabled, and the level has changed, request a meter report:\n    else if (state.proactiveReports & levelEvent.isStateChange) {\n        result << response([\"delay 5000\", secure(zwave.meterV3.meterGet(scale: 2)),\"delay 10000\", secure(zwave.meterV3.meterGet(scale: 2))])\n        // Meter request is delayed for 5s, although sometimes this isn't long enough, so make a second request after another 10 seconds.\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_ALL V1 (0x27) : SWITCH_ALL_REPORT )\n *\n *  The All Switch Report Command is used to report if the device is included or excluded from the all on/all off\n *  functionality.\n *\n *  Note: The Fibaro Dimmer 2 supports control of this functionality via Parameter #11, in addition to\n *  SWITCH_ALL_SET commands.\n *\n *  Action: Log an info message.\n *\n *  cmd attributes:\n *    Short    mode\n *      0   = MODE_EXCLUDED_FROM_THE_ALL_ON_ALL_OFF_FUNCTIONALITY\n *      1   = MODE_EXCLUDED_FROM_THE_ALL_ON_FUNCTIONALITY_BUT_NOT_ALL_OFF\n *      2   = MODE_EXCLUDED_FROM_THE_ALL_OFF_FUNCTIONALITY_BUT_NOT_ALL_ON\n *      255 = MODE_INCLUDED_IN_THE_ALL_ON_ALL_OFF_FUNCTIONALITY\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchallv1.SwitchAllReport cmd) {\n    logger(\"zwaveEvent(): Switch All Report received: ${cmd}\",\"trace\")\n\n    def msg = \"\"\n    switch (cmd.mode) {\n            case 0:\n                msg = \"Device is excluded from the all on/all off functionality.\"\n                break\n\n            case 1:\n                msg = \"Device is excluded from the all on functionality but not all off.\"\n                break\n\n            case 2:\n                msg = \"Device is excluded from the all off functionality but not all on.\"\n                break\n\n            default:\n                msg = \"Device is included in the all on/all off functionality.\"\n                break\n    }\n\n    logger(\"Switch All Mode: ${msg}\",\"info\")\n\n    return msg\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SCENE_ACTIVATION (0x2B) : SCENE_ACTIVATION_SET )\n *\n *  The Scene Activation Set Command is used to activate the setting associated to the scene ID.\n *\n *  Action: Raise scene event and log an info message.\n *\n *  cmd attributes:\n *    Short    dimmingDuration\n *      0x00       = Instantly\n *      0x01..0x7F = 1 second (0x01) to 127 seconds (0x7F) in 1-second resolution.\n *      0x80..0xFE = 1 minute (0x80) to 127 minutes (0xFE) in 1-minute resolution.\n *      0xFF       = Dimming duration configured by the Scene Actuator Configuration Set and Scene\n *                   Controller Configuration Set Command depending on device used.\n *    Short    sceneId\n *      0x00..0xFF = Scene0..Scene255\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) {\n    logger(\"zwaveEvent(): Scene Activation Set received: ${cmd}\",\"trace\")\n\n    def result = []\n    result << createEvent(name: \"scene\", value: \"$cmd.sceneId\", data: [switchType: \"$settings.param20\"], descriptionText: \"Scene id ${cmd.sceneId} was activated\", isStateChange: true)\n\n    logger(\"Scene #${cmd.sceneId} was activated.\",\"info\")\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SENSOR_MULTILEVEL V4 (0x31) : SENSOR_MULTILEVEL_REPORT )\n *\n *  The Multilevel Sensor Report Command is used by a multilevel sensor to advertise a sensor reading.\n *\n *  Action: Raise appropriate type of event (and disp event) and log an info message.\n *\n *  Note: SmartThings does not yet have capabilities corresponding to all possible sensor types, therefore\n *  some of the event types raised below are non-standard.\n *\n *  Note: Fibaro Dimmer 2 appears to report power (sensorType 4) only.\n *\n *  cmd attributes:\n *    Short         precision           Indicates the number of decimals.\n *                                      E.g. The decimal value 1025 with precision 2 is therefore equal to 10.25.\n *    Short         scale               Indicates what unit the sensor uses.\n *    BigDecimal    scaledSensorValue   Sensor value as a double.\n *    Short         sensorType          Sensor Type (8 bits).\n *    List<Short>   sensorValue         Sensor value as an array of bytes.\n *    Short         size                Indicates the number of bytes used for the sensor value.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv4.SensorMultilevelReport cmd) {\n    logger(\"zwaveEvent(): SensorMultilevelReport received: ${cmd}\",\"trace\")\n\n    def result = []\n    def map = [ displayed: true, value: cmd.scaledSensorValue.toString() ]\n    def dispMap = [ displayed: false ]\n\n    // Sensor Types up to V4 only, there are further sensor types up to V10 defined.\n    switch (cmd.sensorType) {\n        case 1:  // Air Temperature (V1)\n            map.name = \"temperature\"\n            map.unit = (cmd.scale == 1) ? \"F\" : \"C\"\n            break\n\n        case 2:  // General Purpose (V1)\n            map.name = \"value\"\n            map.unit = (cmd.scale == 1) ? \"\" : \"%\"\n            break\n\n        case 3:  // Luninance (V1)\n            map.name = \"illuminance\"\n            map.unit = (cmd.scale == 1) ? \"lux\" : \"%\"\n            break\n\n        case 4:  // Power (V2)\n            map.name = \"power\"\n            map.unit = (cmd.scale == 1) ? \"Btu/h\" : \"W\"\n            dispMap.name = \"dispPower\"\n            dispMap.value = String.format(\"%.1f\",cmd.scaledSensorValue as BigDecimal) + \" ${map.unit}\"\n            break\n\n        case 5:  // Humidity (V2)\n            map.name = \"humidity\"\n            map.unit = (cmd.scale == 1) ? \"g/m^3\" : \"%\"\n            break\n\n        case 6:  // Velocity (V2)\n            map.name = \"velocity\"\n            map.unit = (cmd.scale == 1) ? \"mph\" : \"m/s\"\n            break\n\n        case 7:  // Direction (V2)\n            map.name = \"direction\"\n            map.unit = \"\"\n            break\n\n        case 8:  // Atmospheric Pressure (V2)\n        case 9:  // Barometric Pressure (V2)\n            map.name = \"pressure\"\n            map.unit = (cmd.scale == 1) ? \"inHg\" : \"kPa\"\n            break\n\n        case 0xA:  // Solar Radiation (V2)\n            map.name = \"radiation\"\n            map.unit = \"W/m^3\"\n            break\n\n        case 0xB:  // Dew Point (V2)\n            map.name = \"dewPoint\"\n            map.unit = (cmd.scale == 1) ? \"F\" : \"C\"\n            break\n\n        case 0xC:  // Rain Rate (V2)\n            map.name = \"rainRate\"\n            map.unit = (cmd.scale == 1) ? \"in/h\" : \"mm/h\"\n            break\n\n        case 0xD:  // Tide Level (V2)\n            map.name = \"tideLevel\"\n            map.unit = (cmd.scale == 1) ? \"ft\" : \"m\"\n            break\n\n        case 0xE:  // Weight (V3)\n            map.name = \"weight\"\n            map.unit = (cmd.scale == 1) ? \"lbs\" : \"kg\"\n            break\n\n        case 0xF:  // Voltage (V3)\n            map.name = \"voltage\"\n            map.unit = (cmd.scale == 1) ? \"mV\" : \"V\"\n            dispMap.name = \"dispVoltage\"\n            dispMap.value = String.format(\"%.1f\",cmd.scaledSensorValue as BigDecimal) + \" ${map.unit}\"\n            break\n\n        case 0x10:  // Current (V3)\n            map.name = \"current\"\n            map.unit = (cmd.scale == 1) ? \"mA\" : \"A\"\n            dispMap.name = \"dispCurrent\"\n            dispMap.value = String.format(\"%.1f\",cmd.scaledSensorValue as BigDecimal) + \" ${map.unit}\"\n            break\n\n        case 0x11:  // Carbon Dioxide Level (V3)\n            map.name = \"carbonDioxide\"\n            map.unit = \"ppm\"\n            break\n\n        case 0x12:  // Air Flow (V3)\n            map.name = \"fluidFlow\"\n            map.unit = (cmd.scale == 1) ? \"cfm\" : \"m^3/h\"\n            break\n\n        case 0x13:  // Tank Capacity (V3)\n            map.name = \"fluidVolume\"\n            map.unit = (cmd.scale == 0) ? \"ltr\" : (cmd.scale == 1) ? \"m^3\" : \"gal\"\n            break\n\n        case 0x14:  // Distance (V3)\n            map.name = \"distance\"\n            map.unit = (cmd.scale == 0) ? \"m\" : (cmd.scale == 1) ? \"cm\" : \"ft\"\n            break\n\n        default:\n            logger(\"zwaveEvent(): SensorMultilevelReport with unhandled sensorType: ${cmd}\",\"warn\")\n            map.name = \"unknown\"\n            map.unit = \"unknown\"\n            break\n    }\n\n    logger(\"New sensor reading: Name: ${map.name}, Value: ${map.value}, Unit: ${map.unit}\",\"info\")\n\n    result << createEvent(map)\n    if (dispMap.name) { result << createEvent(dispMap) }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_METER V3 (0x32) : METER_REPORT )\n *\n *  The Meter Report Command is used to advertise a meter reading.\n *\n *  Action: Raise appropriate type of event (and disp... event) and log an info message.\n *\n *  Note: Fibaro Dimmer 2 supports energy and power only. It will not report current, voltage, or power factor.\n *\n *  cmd attributes:\n *    Integer        deltaTime                   Time in seconds since last report.\n *    Short          meterType                   Specifies the type of metering device.\n *      0x00 = Unknown\n *      0x01 = Electric meter\n *      0x02 = Gas meter\n *      0x03 = Water meter\n *    List<Short>    meterValue                  Meter value as an array of bytes.\n *    Double         scaledMeterValue            Meter value as a double.\n *    List<Short>    previousMeterValue          Previous meter value as an array of bytes.\n *    Double         scaledPreviousMeterValue    Previous meter value as a double.\n *    Short          size                        The size of the array for the meterValue and previousMeterValue.\n *    Short          scale                       Indicates what unit the sensor uses (dependent on meterType).\n *    Short          precision                   The decimal precision of the values.\n *    Short          rateType                    Specifies if it is import or export values to be read.\n *      0x01 = Import (consumed)\n *      0x02 = Export (produced)\n *    Boolean        scale2                      ???\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {\n    logger(\"zwaveEvent(): Meter Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    switch (cmd.meterType) {\n        case 1:  // Electric meter:\n            switch (cmd.scale) {\n                case 0:  // Accumulated Energy (kWh):\n                    result << createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kWh\", displayed: true)\n                    result << createEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" kWh\", displayed: false)\n                    logger(\"New meter reading: Accumulated Energy: ${cmd.scaledMeterValue} kWh\",\"info\")\n                    break\n\n                case 1:  // Accumulated Energy (kVAh):\n                    result << createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kVAh\", displayed: true)\n                    result << createEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" kVAh\", displayed: false)\n                    logger(\"New meter reading: Accumulated Energy: ${cmd.scaledMeterValue} kVAh\",\"info\")\n                    break\n\n                case 2:  // Instantaneous Power (Watts):\n                    result << createEvent(name: \"power\", value: cmd.scaledMeterValue, unit: \"W\", displayed: true)\n                    result << createEvent(name: \"dispPower\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" W\", displayed: false)\n                    logger(\"New meter reading: Instantaneous Power: ${cmd.scaledMeterValue} W\",\"info\")\n                    break\n\n                case 3:  // Accumulated Pulse Count:\n                    result << createEvent(name: \"pulseCount\", value: cmd.scaledMeterValue, unit: \"\", displayed: true)\n                    logger(\"New meter reading: Accumulated Electricity Pulse Count: ${cmd.scaledMeterValue}\",\"info\")\n                    break\n\n                case 4:  // Instantaneous Voltage (Volts):\n                    result << createEvent(name: \"voltage\", value: cmd.scaledMeterValue, unit: \"V\", displayed: true)\n                    result << createEvent(name: \"dispVoltage\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" V\", displayed: false)\n                    logger(\"New meter reading: Instantaneous Voltage: ${cmd.scaledMeterValue} V\",\"info\")\n                    break\n\n                 case 5:  // Instantaneous Current (Amps):\n                    result << createEvent(name: \"current\", value: cmd.scaledMeterValue, unit: \"A\", displayed: true)\n                    result << createEvent(name: \"dispCurrent\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" V\", displayed: false)\n                    logger(\"New meter reading: Instantaneous Current: ${cmd.scaledMeterValue} A\",\"info\")\n                    break\n\n                 case 6:  // Instantaneous Power Factor:\n                    result << createEvent(name: \"powerFactor\", value: cmd.scaledMeterValue, unit: \"\", displayed: true)\n                    result << createEvent(name: \"dispPowerFactor\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal), displayed: false)\n                    logger(\"New meter reading: Instantaneous Power Factor: ${cmd.scaledMeterValue}\",\"info\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Meter Report with unhandled scale: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        case 2:  // Gas meter:\n\n            switch (cmd.scale) {\n                case 0:  // Accumulated Gas Volume (m^3):\n                    result << createEvent(name: \"fluidVolume\", value: cmd.scaledMeterValue, unit: \"m^3\", displayed: true)\n                    result << createEvent(name: \"dispFluidVolume\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" m^3\", displayed: false)\n                    logger(\"New meter reading: Accumulated Gas Volume: ${cmd.scaledMeterValue} m^3\",\"info\")\n                    break\n\n                case 1:  // Accumulated Gas Volume (ft^3):\n                    result << createEvent(name: \"fluidVolume\", value: cmd.scaledMeterValue, unit: \"ft^3\", displayed: true)\n                    result << createEvent(name: \"dispFluidVolume\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" ft^3\", displayed: false)\n                    logger(\"New meter reading: Accumulated Gas Volume: ${cmd.scaledMeterValue} ft^3\",\"info\")\n                    break\n\n                case 3:  // Accumulated Pulse Count:\n                    result << createEvent(name: \"pulseCount\", value: cmd.scaledMeterValue, unit: \"\", displayed: true)\n                    logger(\"New meter reading: Accumulated Gas Pulse Count: ${cmd.scaledMeterValue}\",\"info\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Meter Report with unhandled scale: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        case 3:  // Water meter:\n\n            switch (cmd.scale) {\n                case 0:  // Accumulated Water Volume (m^3):\n                    result << createEvent(name: \"fluidVolume\", value: cmd.scaledMeterValue, unit: \"m^3\", displayed: true)\n                    result << createEvent(name: \"dispFluidVolume\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" m^3\", displayed: false)\n                    logger(\"New meter reading: Accumulated Water Volume: ${cmd.scaledMeterValue} m^3\",\"info\")\n                    break\n\n                case 1:  // Accumulated Water Volume (ft^3):\n                    result << createEvent(name: \"fluidVolume\", value: cmd.scaledMeterValue, unit: \"ft^3\", displayed: true)\n                    result << createEvent(name: \"dispFluidVolume\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" ft^3\", displayed: false)\n                    logger(\"New meter reading: Accumulated Water Volume: ${cmd.scaledMeterValue} ft^3\",\"info\")\n                    break\n\n                case 2:  // Accumulated Water Volume (US gallons):\n                    result << createEvent(name: \"fluidVolume\", value: cmd.scaledMeterValue, unit: \"gal\", displayed: true)\n                    result << createEvent(name: \"dispFluidVolume\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" gal\", displayed: false)\n                    logger(\"New meter reading: Accumulated Water Volume: ${cmd.scaledMeterValue} gal\",\"info\")\n                    break\n\n                case 3:  // Accumulated Pulse Count:\n                    result << createEvent(name: \"pulseCount\", value: cmd.scaledMeterValue, unit: \"\", displayed: true)\n                    logger(\"New meter reading: Accumulated Water Pulse Count: ${cmd.scaledMeterValue}\",\"info\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Meter Report with unhandled scale: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        default:\n            logger(\"zwaveEvent(): Meter Report with unhandled meterType: ${cmd}\",\"warn\")\n            break\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_CRC16_ENCAP V1 (0x56) : CRC_16_ENCAP )\n *\n *  The CRC-16 Encapsulation Command Class is used to encapsulate a command with an additional CRC-16 checksum\n *  to ensure integrity of the payload. The purpose for this command class is to ensure a higher integrity level\n *  of payloads carrying important data.\n *\n *  Action: Extract the encapsulated command and pass to zwaveEvent().\n *\n *  Note: Validation of the checksum is not necessary as this is performed by the hub.\n *\n *  cmd attributes:\n *    Integer      checksum      Checksum.\n *    Short        command       Command identifier of the embedded command.\n *    Short        commandClass  Command Class identifier of the embedded command.\n *    List<Short>  data          Embedded command data.\n *\n *  Example: Crc16Encap(checksum: 125, command: 2, commandClass: 50, data: [33, 68, 0, 0, 0, 194, 0, 0, 77])\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {\n    logger(\"zwaveEvent(): CRC-16 Encapsulation Command received: ${cmd}\",\"trace\")\n\n    def versions = getCommandClassVersions()\n    def version = versions[cmd.commandClass as Integer]\n    def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)\n    def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)\n    // TO DO: It should be possible to replace the lines above with this line soon...\n    //def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())\n    if (!encapsulatedCommand) {\n        logger(\"zwaveEvent(): Could not extract command from ${cmd}\",\"error\")\n    } else {\n        return zwaveEvent(encapsulatedCommand)\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_DEVICE_RESET_LOCALLY V1 (0x5A) : DEVICE_RESET_LOCALLY_NOTIFICATION )\n *\n *  The Device Reset Locally Notification Command is used to advertise that the device will be reset.\n *\n *  Action: Log a warn message.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {\n    logger(\"zwaveEvent(): Device Reset Locally Notification: ${cmd}\",\"trace\")\n    logger(\"zwaveEvent(): Device was reset!\",\"warn\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MULTICHANNEL V4 (0x60) : MULTI_CHANNEL_CMD_ENCAP )\n *\n *  The Multi Channel Command Encapsulation command is used to encapsulate commands. Any command supported by\n *  a Multi Channel End Point may be encapsulated using this command.\n *\n *  Action: Extract the encapsulated command and pass to the appropriate zwaveEvent() handler.\n *\n *  Note: We only receive these commands from a Dimmer 2 if the hub has been added to one or more association\n *  groups 2-5, which is not normally needed. The sourceEndPoint attribute will indicate if from S1 or S2, but we\n *  don't care here, because button presses are handled via SCENE_ACTIVATION_SET commands instead.\n *\n *  cmd attributes:\n *    Boolean      bitAddress           Set to true if multicast addressing is used.\n *    Short        command              Command identifier of the embedded command.\n *    Short        commandClass         Command Class identifier of the embedded command.\n *    Short        destinationEndPoint  Destination End Point.\n *    List<Short>  parameter            Carries the parameter(s) of the embedded command.\n *    Short        sourceEndPoint       Source End Point.\n *\n *  Example: MultiChannelCmdEncap(bitAddress: false, command: 1, commandClass: 32, destinationEndPoint: 0,\n *            parameter: [0], sourceEndPoint: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {\n    logger(\"zwaveEvent(): Multi Channel Command Encapsulation command received: ${cmd}\",\"trace\")\n\n    def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())\n    if (!encapsulatedCommand) {\n        logger(\"zwaveEvent(): Could not extract command from ${cmd}\",\"error\")\n    } else {\n        return zwaveEvent(encapsulatedCommand)\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_CONFIGURATION V1 (0x70) : CONFIGURATION_REPORT )\n *\n *  The Configuration Report Command is used to advertise the actual value of the advertised parameter.\n *\n *  Action: Store the value in the parameter cache, update syncPending, and log an info message.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  cmd attributes:\n *    List<Short>  configurationValue  Value of parameter (byte array).\n *    Short        parameterNumber     Parameter ID.\n *    Short        size                Size of parameter's value (bytes).\n *\n *  Example: ConfigurationReport(configurationValue: [0], parameterNumber: 14, reserved11: 0,\n *            scaledConfigurationValue: 0, size: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {\n    logger(\"zwaveEvent(): Configuration Report received: ${cmd}\",\"trace\")\n\n    state.\"paramCache${cmd.parameterNumber}\" = cmd.scaledConfigurationValue.toInteger()\n    def paramName = getParamsMd().find( { it.id == cmd.parameterNumber }).name\n    logger(\"Parameter #${cmd.parameterNumber} [${paramName}] has value: ${cmd.scaledConfigurationValue}\",\"info\")\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_NOTIFICATION V3 (0x71) : NOTIFICATION_REPORT )\n *\n *  The Notification Report Command is used to advertise notification information.\n *\n *  Action: Raise appropriate type of event (e.g. fault, tamper, water) and log an info or warn message.\n *\n *  Note: SmartThings does not yet have official capabilities definited for many types of notification. E.g. this\n *  handler raises 'fault' events, which is not part of any standard capability.\n *\n *  cmd attributes:\n *    Short        event                  Event Type (see code below).\n *    List<Short>  eventParameter         Event Parameter(s) (depends on Event type).\n *    Short        eventParametersLength  Length of eventParameter.\n *    Short        notificationStatus     The notification reporting status of the device (depends on push or pull model).\n *    Short        notificationType       Notification Type (see code below).\n *    Boolean      sequence\n *    Short        v1AlarmLevel           Legacy Alarm Level from Alarm CC V1.\n *    Short        v1AlarmType            Legacy Alarm Type from Alarm CC V1.\n *    Short        zensorNetSourceNodeId  Source node ID\n *\n *  Example: NotificationReport(event: 8, eventParameter: [], eventParametersLength: 0, notificationStatus: 255,\n *    notificationType: 8, reserved61: 0, sequence: false, v1AlarmLevel: 0, v1AlarmType: 0, zensorNetSourceNodeId: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {\n    logger(\"zwaveEvent(): Notification Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    switch (cmd.notificationType) {\n        //case 1:  // Smoke Alarm: // Not Implemented yet. Should raise smoke/carbonMonoxide/consumableStatus events etc...\n        //case 2:  // CO Alarm:\n        //case 3:  // CO2 Alarm:\n\n        case 4:  // Heat Alarm:\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                    // Do not send a fault clear event automatically.\n                    logger(\"Heat Alarm Cleared\",\"info\")\n                    break\n\n                case 1:  // Overheat detected:\n                case 2:  // Overheat detected, Unknown Location:\n                    result << createEvent(name: \"fault\", value: \"overheat\", descriptionText: \"Overheat detected!\", displayed: true)\n                    logger(\"Overheat detected!\",\"warn\")\n                    break\n\n                case 3:  // Rapid Temperature Rise:\n                case 4:  // Rapid Temperature Rise, Unknown Location:\n                    result << createEvent(name: \"fault\", value: \"temperature\", descriptionText: \"Rapid temperature rise detected!\", displayed: true)\n                    logger(\"Rapid temperature rise detected!\",\"warn\")\n                    break\n\n                case 5:  // Underheat detected:\n                case 6:  // Underheat detected, Unknown Location:\n                    result << createEvent(name: \"fault\", value: \"underheat\", descriptionText: \"Underheat detected!\", displayed: true)\n                    logger(\"Underheat detected!\",\"warn\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        //case 5:  // Water Alarm: // Not Implemented yet. Should raise water/consumableStatus events etc...\n\n        case 8:  // Power Management:\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                    // Do not send a fault clear event automatically.\n                    logger(\"Previous Events cleared\",\"info\")\n                    break\n\n                //case 1:  // Mains Connected:\n                //case 2:  // AC Mains Disconnected:\n                //case 3:  // AC Mains Re-connected:\n\n                case 4:  // Surge:\n                    result << createEvent(name: \"fault\", value: \"surge\", descriptionText: \"Power surge detected!\", displayed: true)\n                    logger(\"Power surge detected!\",\"warn\")\n                    break\n\n                case 5:  // Voltage Drop:\n                    result << createEvent(name: \"fault\", value: \"voltage\", descriptionText: \"Voltage drop detected!\", displayed: true)\n                    logger(\"Voltage drop detected!\",\"warn\")\n                    break\n\n                case 6:  // Over-current:\n                    result << createEvent(name: \"fault\", value: \"current\", descriptionText: \"Over-current detected!\", displayed: true)\n                    logger(\"Over-current detected!\",\"warn\")\n                    break\n\n                 case 7:  // Over-Voltage:\n                    result << createEvent(name: \"fault\", value: \"voltage\", descriptionText: \"Over-voltage detected!\", displayed: true)\n                    logger(\"Over-voltage detected!\",\"warn\")\n                    break\n\n                 case 8:  // Overload:\n                    result << createEvent(name: \"fault\", value: \"load\", descriptionText: \"Overload detected!\", displayed: true)\n                    logger(\"Overload detected!\",\"warn\")\n                    break\n\n                 case 9:  // Load Error:\n                    result << createEvent(name: \"fault\", value: \"load\", descriptionText: \"Load Error detected!\", displayed: true)\n                    logger(\"Load Error detected!\",\"warn\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        case 9:  // system:\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                    // Do not send a fault clear event automatically.\n                    logger(\"Previous Events cleared\",\"info\")\n                    break\n\n                case 1:  // Harware Failure:\n                case 3:  // Harware Failure (with manufacturer proprietary failure code):\n                    result << createEvent(name: \"fault\", value: \"hardware\", descriptionText: \"Hardware failure detected!\", displayed: true)\n                    logger(\"Hardware failure detected!\",\"warn\")\n                    break\n\n                case 2:  // Software Failure:\n                case 4:  // Software Failure (with manufacturer proprietary failure code):\n                    result << createEvent(name: \"fault\", value: \"firmware\", descriptionText: \"Firmware failure detected!\", displayed: true)\n                    logger(\"Firmware failure detected!\",\"warn\")\n                    break\n\n                case 6:  // Tampering:\n                    result << createEvent(name: \"tamper\", value: \"detected\", descriptionText: \"Tampering: Product covering removed!\", displayed: true)\n                    logger(\"Tampering: Product covering removed!\",\"warn\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        default:\n            logger(\"zwaveEvent(): Notification Report recieved with unhandled notificationType: ${cmd}\",\"warn\")\n            break\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MANUFACTURER_SPECIFIC V2 (0x72) : MANUFACTURER_SPECIFIC_REPORT )\n *\n *  Manufacturer-Specific Reports are used to advertise manufacturer-specific information, such as product number\n *  and serial number.\n *\n *  Action: Publish values as device 'data'. Log a warn message if manufacturerId and/or productId do not\n *  correspond to Fibaro Dimmer 2.\n *\n *  Example: ManufacturerSpecificReport(manufacturerId: 271, manufacturerName: Fibargroup, productId: 4096,\n *   productTypeId: 258)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {\n    logger(\"zwaveEvent(): Manufacturer-Specific Report received: ${cmd}\",\"trace\")\n\n    // Display as hex strings:\n    def manufacturerIdDisp = String.format(\"%04X\",cmd.manufacturerId)\n    def productIdDisp = String.format(\"%04X\",cmd.productId)\n    def productTypeIdDisp = String.format(\"%04X\",cmd.productTypeId)\n\n    logger(\"Manufacturer-Specific Report: Manufacturer ID: ${manufacturerIdDisp}, Manufacturer Name: ${cmd.manufacturerName}\" +\n    \", Product Type ID: ${productTypeIdDisp}, Product ID: ${productIdDisp}\",\"info\")\n\n    if ( 271 != cmd.manufacturerId) logger(\"Device Manufacturer is not Fibaro. Using this device handler with a different device may damage your device!\",\"warn\")\n    if ( 4096 != cmd.productId) logger(\"Product ID does not match Fibaro Dimmer 2. Using this device handler with a different device may damage you device!\",\"warn\")\n\n    updateDataValue(\"manufacturerName\",cmd.manufacturerName)\n    updateDataValue(\"manufacturerId\",manufacturerIdDisp)\n    updateDataValue(\"productId\",productIdDisp)\n    updateDataValue(\"productTypeId\",productTypeIdDisp)\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_POWERLEVEL V1 (0x73) : POWERLEVEL_REPORT )\n *\n *  The Powerlevel Report is used to advertise the current RF transmit power of the device.\n *\n *  Action: Log an info message.\n *\n *  cmd attributes:\n *    Short  powerLevel  The current power level indicator value in effect on the node\n *    Short  timeout     The time in seconds the node has at Power level before resetting to normal Power level.\n *\n *  Example: PowerlevelReport(powerLevel: 0, timeout: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.powerlevelv1.PowerlevelReport cmd) {\n    logger(\"zwaveEvent(): Powerlevel Report received: ${cmd}\",\"trace\")\n    def power = (cmd.powerLevel > 0) ? \"minus${cmd.powerLevel}dBm\" : \"NormalPower\"\n    logger(\"Powerlevel Report: Power: ${power}, Timeout: ${cmd.timeout}\",\"info\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_PROTECTION V2 (0x75) : PROTECTION_REPORT )\n *\n *  The Protection Report is used to report the protection state of a device.\n *  I.e. measures to prevent unintentional control (e.g. by a child).\n *\n *  Action: Cache values, update syncPending, and log an info message.\n *\n *  cmd attributes:\n *    Short  localProtectionState  Local protection state (i.e. physical switches/buttons)\n *    Short  rfProtectionState     RF protection state.\n *\n *  Example: ProtectionReport(localProtectionState: 0, reserved01: 0, reserved11: 0, rfProtectionState: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.protectionv2.ProtectionReport cmd) {\n    logger(\"zwaveEvent(): Protection Report received: ${cmd}\",\"trace\")\n\n    state.protectLocalCache = cmd.localProtectionState\n    state.protectRFCache = cmd.rfProtectionState\n\n    def lp, rfp = \"\"\n\n    switch(cmd.localProtectionState)  {\n        case 0:\n            lp = \"Unprotected\"\n            break\n        case 1:\n            lp = \"Protection by sequence\"\n            break\n        case 2:\n            lp = \"No operation possible\"\n            break\n        default:\n            lp = \"Unknwon\"\n            break\n\n    }\n\n    switch(cmd.rfProtectionState)  {\n        case 0:\n            rfp = \"Unprotected\"\n            break\n        case 1:\n            rfp = \"No RF Control\"\n            break\n        case 2:\n            rfp = \"No RF Response\"\n            break\n        default:\n            rfp = \"Unknwon\"\n            break\n    }\n\n    logger(\"Protection Report: Local Protection: ${lp}, RF Protection: ${rfp}\",\"info\")\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_FIRMWARE_UPDATE_MD V2 (0x7A) : FirmwareMdReport )\n *\n *  The Firmware Meta Data Report Command is used to advertise the status of the current firmware in the device.\n *\n *  Action: Publish values as device 'data' and log an info message. No check is performed.\n *\n *  cmd attributes:\n *    Integer  checksum        Checksum of the firmware image.\n *    Integer  firmwareId      Firware ID (this is not the firmware version).\n *    Integer  manufacturerId  Manufacturer ID.\n *\n *  Example: FirmwareMdReport(checksum: 50874, firmwareId: 274, manufacturerId: 271)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd) {\n    logger(\"zwaveEvent(): Firmware Metadata Report received: ${cmd}\",\"trace\")\n\n    // Display as hex strings:\n    def firmwareIdDisp = String.format(\"%04X\",cmd.firmwareId)\n    def checksumDisp = String.format(\"%04X\",cmd.checksum)\n\n    logger(\"Firmware Metadata Report: Firmware ID: ${firmwareIdDisp}, Checksum: ${checksumDisp}\",\"info\")\n\n    updateDataValue(\"firmwareId\",\"${firmwareIdDisp}\")\n    updateDataValue(\"firmwareChecksum\",\"${checksumDisp}\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_ASSOCIATION V2 (0x85) : ASSOCIATION_REPORT )\n *\n *  The Association Report command is used to advertise the current destination nodes of a given association group.\n *\n *  Action: Log info message only. Do not cache values as the Fibaro Dimmer 2 uses COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  Example: AssociationReport(groupingIdentifier: 4, maxNodesSupported: 5, nodeId: [1], reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {\n    logger(\"zwaveEvent(): Association Report received: ${cmd}\",\"trace\")\n\n    //state.\"assocGroupCache${cmd.groupingIdentifier}\" = cmd.nodeId\n\n    // Display to user in hex format (same as IDE):\n    def hexArray  = []\n    cmd.nodeId.sort().each { hexArray.add(String.format(\"%02X\", it)) };\n    logger(\"Association Group ${cmd.groupingIdentifier} contains nodes: ${hexArray} (hexadecimal format)\",\"info\")\n\n    //updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_VERSION V1 (0x86) : VERSION_REPORT )\n *\n *  The Version Report Command is used to advertise the library type, protocol version, and application version.\n\n *  Action: Publish values as device 'data' and log an info message. No check is performed.\n *\n *  Note: Device actually supports V2, but SmartThings only supports V1.\n *\n *  cmd attributes:\n *    Short  applicationSubVersion\n *    Short  applicationVersion\n *    Short  zWaveLibraryType\n *    Short  zWaveProtocolSubVersion\n *    Short  zWaveProtocolVersion\n *\n *  Example: VersionReport(applicationSubVersion: 4, applicationVersion: 3, zWaveLibraryType: 3,\n *   zWaveProtocolSubVersion: 5, zWaveProtocolVersion: 4)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {\n    logger(\"zwaveEvent(): Version Report received: ${cmd}\",\"trace\")\n\n    def zWaveLibraryTypeDisp  = String.format(\"%02X\",cmd.zWaveLibraryType)\n    def zWaveLibraryTypeDesc  = \"\"\n    switch(cmd.zWaveLibraryType) {\n        case 1:\n            zWaveLibraryTypeDesc = \"Static Controller\"\n            break\n\n        case 2:\n            zWaveLibraryTypeDesc = \"Controller\"\n            break\n\n        case 3:\n            zWaveLibraryTypeDesc = \"Enhanced Slave\"\n            break\n\n        case 4:\n            zWaveLibraryTypeDesc = \"Slave\"\n            break\n\n        case 5:\n            zWaveLibraryTypeDesc = \"Installer\"\n            break\n\n        case 6:\n            zWaveLibraryTypeDesc = \"Routing Slave\"\n            break\n\n        case 7:\n            zWaveLibraryTypeDesc = \"Bridge Controller\"\n            break\n\n        case 8:\n            zWaveLibraryTypeDesc = \"Device Under Test (DUT)\"\n            break\n\n        case 0x0A:\n            zWaveLibraryTypeDesc = \"AV Remote\"\n            break\n\n        case 0x0B:\n            zWaveLibraryTypeDesc = \"AV Device\"\n            break\n\n        default:\n            zWaveLibraryTypeDesc = \"N/A\"\n    }\n\n    def applicationVersionDisp = String.format(\"%d.%02d\",cmd.applicationVersion,cmd.applicationSubVersion)\n    def zWaveProtocolVersionDisp = String.format(\"%d.%02d\",cmd.zWaveProtocolVersion,cmd.zWaveProtocolSubVersion)\n\n    logger(\"Version Report: Application Version: ${applicationVersionDisp}, \" +\n           \"Z-Wave Protocol Version: ${zWaveProtocolVersionDisp}, \" +\n           \"Z-Wave Library Type: ${zWaveLibraryTypeDisp} (${zWaveLibraryTypeDesc})\",\"info\")\n\n    updateDataValue(\"applicationVersion\",\"${cmd.applicationVersion}\")\n    updateDataValue(\"applicationSubVersion\",\"${cmd.applicationSubVersion}\")\n    updateDataValue(\"zWaveLibraryType\",\"${zWaveLibraryTypeDisp}\")\n    updateDataValue(\"zWaveProtocolVersion\",\"${cmd.zWaveProtocolVersion}\")\n    updateDataValue(\"zWaveProtocolSubVersion\",\"${cmd.zWaveProtocolSubVersion}\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION V2 (0x8E) : ASSOCIATION_REPORT )\n *\n *  The Multi-channel Association Report command is used to advertise the current destinations of a given\n *  association group (nodes and endpoints).\n *\n *  Action: Store the destinations in the assocGroup cache, update syncPending, and log an info message.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  Example: MultiChannelAssociationReport(groupingIdentifier: 2, maxNodesSupported: 8, nodeId: [9,0,1,1,2,3],\n *            reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) {\n    logger(\"zwaveEvent(): Multi-Channel Association Report received: ${cmd}\",\"trace\")\n\n    state.\"assocGroupCache${cmd.groupingIdentifier}\" = cmd.nodeId // Must not sort as order is important.\n\n    def assocGroupName = getAssocGroupsMd().find( { it.id == cmd.groupingIdentifier} ).name\n    // Display to user in hex format (same as IDE):\n    def hexArray  = []\n    cmd.nodeId.each { hexArray.add(String.format(\"%02X\", it)) };\n    logger(\"Association Group #${cmd.groupingIdentifier} [${assocGroupName}] contains destinations: ${hexArray}\",\"info\")\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SECURITY V1 (0x98) : SECURITY_COMMANDS_SUPPORTED_REPORT )\n *\n *  The Security Commands Supported Report command advertises which command classes are supported using security\n *  encapsulation.\n *\n *  Action: Store the list of supported command classes in state.secureCommandClasses. Log info message.\n *\n *  Example:  SecurityCommandsSupportedReport(commandClassControl: [43],\n *   commandClassSupport: [32, 90, 133, 38, 142, 96, 112, 117, 39], reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {\n    logger(\"zwaveEvent(): Security Commands Supported Report received: ${cmd}\",\"trace\")\n\n    state.secureCommandClasses = cmd.commandClassSupport\n\n    // Display to user in hex format (same as IDE):\n    def hexArray  = []\n    cmd.commandClassSupport.sort().each { hexArray.add(String.format(\"0x%02X\", it)) };\n    logger(\"Security Commands Supported: ${hexArray}\",\"info\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SECURITY V1 (0x98) : SECURITY_MESSAGE_ENCAPSULATION )\n *\n *  The Security Message Encapsulation command is used to encapsulate Z-Wave commands using AES-128.\n *\n *  Action: Extract the encapsulated command and pass to the appropriate zwaveEvent() handler.\n *\n *  cmd attributes:\n *    List<Short> commandByte         Parameters of the encapsulated command.\n *    Short   commandClassIdentifier  Command Class ID of the encapsulated command.\n *    Short   commandIdentifier       Command ID of the encapsulated command.\n *    Boolean secondFrame             Indicates if first or second frame.\n *    Short   sequenceCounter\n *    Boolean sequenced               True if the command is transmitted using multiple frames.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {\n    logger(\"zwaveEvent(): Security Encapsulated Command received: ${cmd}\",\"trace\")\n\n    def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())\n    if (encapsulatedCommand) {\n        return zwaveEvent(encapsulatedCommand)\n    } else {\n        logger(\"zwaveEvent(): Unable to extract security encapsulated command from: ${cmd}\",\"error\")\n    }\n}\n\n/**\n *  zwaveEvent( DEFAULT CATCHALL )\n *\n *  Called for all commands that aren't handled above.\n **/\ndef zwaveEvent(physicalgraph.zwave.Command cmd) {\n    logger(\"zwaveEvent(): No handler for command: ${cmd}\",\"error\")\n}\n\n\n/*****************************************************************************************************************\n *  Capability-related Commands:\n *****************************************************************************************************************/\n\n/**\n *  on()                        [Capability: Switch]\n *\n *  Turn the dimmer on.\n **/\ndef on() {\n    logger(\"on(): Turning dimmer on.\",\"info\")\n    def cmds = []\n    cmds << zwave.basicV1.basicSet(value: 0xFF)\n    if (state.proactiveReports) cmds << zwave.switchMultilevelV1.switchMultilevelGet()\n    sendSecureSequence(cmds,5000)\n}\n\n/**\n *  off()                       [Capability: Switch]\n *\n *  Turn the dimmer off.\n **/\ndef off() {\n    logger(\"off(): Turning dimmer off.\",\"info\")\n    def cmds = []\n    cmds << zwave.basicV1.basicSet(value: 0x00)\n    if (state.proactiveReports) cmds << zwave.switchMultilevelV1.switchMultilevelGet()\n    sendSecureSequence(cmds,5000)\n}\n\n/**\n *  setLevel()                  [Capability: Switch Level]\n *\n *  Set the dimmer level.\n *\n *  Parameters:\n *   level    Target level (0-100%).\n **/\ndef setLevel(level) {\n    logger(\"setLevel(${level})\",\"trace\")\n\n    if (level < 0) level = 0\n    if (level > 100) level = 100\n    logger(\"Setting dimmer to ${level}%\",\"info\")\n\n    // Clear nightmodePendingLevel as it's been overridden.\n    state.nightmodePendingLevel = 0\n\n    def cmds = []\n    cmds << zwave.basicV1.basicSet(value: Math.round(level * 99 / 100 )) // Convert from 0-100 to 0-99.\n    if (state.proactiveReports) cmds << zwave.switchMultilevelV1.switchMultilevelGet()\n    sendSecureSequence(cmds,5000)\n}\n\n/**\n *  refresh()                   [Capability: Refresh]\n *\n *  Request switchMultilevel, energy, and power reports.\n *  Also, force a configuration sync.\n **/\ndef refresh() {\n    logger(\"refresh()\",\"trace\")\n\n    def cmds = []\n    cmds << zwave.switchMultilevelV1.switchMultilevelGet()\n    cmds << zwave.meterV3.meterGet(scale: 0)\n    cmds << zwave.meterV3.meterGet(scale: 2)\n\n    sendSecureSequence(cmds,200)\n    sync()\n}\n\n/**\n *  poll()                      [Capability: Polling]\n *\n *  Calls refresh().\n **/\ndef poll() {\n    logger(\"poll()\",\"trace\")\n    refresh()\n}\n\n/*****************************************************************************************************************\n *  Custom Commands:\n *****************************************************************************************************************/\n\n/**\n *  reset()\n *\n *  Calls resetEnergy().\n *\n *  Note: this used to be part of the official 'Energy Meter' capability, but isn't anymore.\n **/\ndef reset() {\n    logger(\"reset()\",\"trace\")\n    resetEnergy()\n}\n\n/**\n *  resetEnergy()\n *\n *  Reset the Accumulated Energy figure held in the device.\n *\n *  Does not return a list of commands, it sends them immediately using sendSecureSequence(). This is required if\n *  triggered by schedule().\n **/\ndef resetEnergy() {\n    logger(\"resetEnergy(): Resetting Accumulated Energy\",\"info\")\n\n    state.energyLastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"energyLastReset\", value: state.energyLastReset, descriptionText: \"Accumulated Energy Reset\")\n\n    sendSecureSequence([\n        zwave.meterV3.meterReset(),\n        zwave.meterV3.meterGet(scale: 0)\n    ],400)\n}\n\n/**\n *  enableNightmode(level)\n *\n *  Force switch-on illuminance level.\n *\n *  Does not return a list of commands, it sends them immediately using sendSecureSequence(). This is required if\n *  triggered by schedule().\n **/\ndef enableNightmode(level=-1) {\n    logger(\"enableNightmode(${level})\",\"info\")\n\n    // Clean level value:\n    if (level == -1) level = settings.configNightmodeLevel.toInteger()\n    if (level > 100) level = 100\n    if (level < 1) level = 1\n\n    // If nightmode is not already active, save last active level and current value of param19, so they can be restored when nightmode is stopped:\n    if (!state.nightmodeActive) {\n\n        state.nightmodePriorLevel = state.lastActiveLevel\n        logger(\"enableNightmode(): Saved previous active level: ${state.nightmodePriorLevel}\",\"info\")\n\n        if (!state.paramCache19) state.paramCache19 = 0\n        state.nightmodePriorParam19 = state.paramCache19.toInteger()\n        logger(\"enableNightmode(): Saved previous param19: ${state.paramCache19}\",\"info\")\n    }\n\n    // If the dimmer is already on, and configNightmodeForce is enabled, then adjust the level immediately:\n    if ((\"on\" == device.latestValue(\"switch\")) & (\"true\" == configNightmodeForce)) sendSecureSequence([zwave.basicV1.basicSet(value: Math.round(level * 99 / 100 ))])\n\n    state.nightmodeActive = true\n    sendEvent(name: \"nightmode\", value: \"Enabled\", descriptionText: \"Nightmode Enabled\", isStateChange: true)\n\n    // Update parameter #19 for force next switch-on level:\n    state.paramTarget19 = level.toInteger()\n    sync()\n}\n\n/**\n *  disableNightmode()\n *\n *  Stop nightmode and restore previous values.\n *\n *  Does not return a list of commands, it sends them immediately using sendSecureSequence(). This is required if\n *  triggered by schedule().\n **/\ndef disableNightmode() {\n    logger(\"disableNightmode()\",\"info\")\n\n    // If nightmode is active, restore param19:\n    if (state.nightmodeActive) {\n\n        logger(\"disableNightmode(): Restoring previous value of param19 to: ${state.nightmodePriorParam19}\",\"debug\")\n        state.paramTarget19 = state.nightmodePriorParam19\n        sync()\n\n        if (state.nightmodePriorLevel > 0) {\n            if ((\"on\" == device.latestValue(\"switch\")) & (\"true\" == configNightmodeForce)) {\n                // Dimmer is already on and configNightmodeForce is enabled, so adjust the level immediately:\n                logger(\"disableNightmode(): Restoring level to: ${state.nightmodePriorLevel}\",\"debug\")\n                sendSecureSequence([zwave.basicV1.basicSet(value: Math.round(state.nightmodePriorLevel.toInteger() * 99 / 100 ))])\n            } else if (0 == state.nightmodePriorParam19) {\n                // Dimmer is off (or configNightmodeForce is not enabled), so need to set a flag to restore the level after it's switched on again, but only if param19 is zero.\n                logger(\"disableNightmode(): Setting flag to restore level at next switch-on: ${state.nightmodePriorLevel}\",\"debug\")\n                state.nightmodePendingLevel = state.nightmodePriorLevel\n            }\n        }\n    }\n\n    state.nightmodeActive = false\n    sendEvent(name: \"nightmode\", value: \"Disabled\", descriptionText: \"Nightmode Disabled\", isStateChange: true)\n}\n\n/**\n *  toggleNightmode()\n **/\ndef toggleNightmode() {\n    logger(\"toggleNightmode()\",\"trace\")\n\n    if (state.nightmodeActive) {\n        disableNightmode()\n    }\n    else {\n        enableNightmode(configNightmodeLevel)\n    }\n}\n\n/**\n *  clearFault()\n *\n *  Clear all active faults.\n **/\ndef clearFault() {\n    logger(\"clearFault(): Clearing active faults.\",\"info\")\n    sendEvent(name: \"fault\", value: \"clear\", descriptionText: \"Fault cleared\", displayed: true)\n}\n\n/*****************************************************************************************************************\n *  SmartThings System Commands:\n *****************************************************************************************************************/\n\n/**\n *  installed()\n *\n *  Runs when the device is first installed.\n *\n *  Action: Set initial values for internal state, and request a full configuration report from the device.\n **/\ndef installed() {\n    log.trace \"installed()\"\n\n    state.installedAt = now()\n    state.energyLastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    state.loggingLevelIDE     = 3\n    state.loggingLevelDevice  = 2\n    state.protectLocalTarget  = 0\n    state.protectRFTarget     = 0\n\n    sendEvent(name: \"fault\", value: \"clear\", descriptionText: \"Fault cleared\", displayed: false)\n\n    refreshConfig()\n}\n\n/**\n *  updated()\n *\n *  Runs when the user hits \"Done\" from Settings page.\n *\n *  Action: Process new settings, sync parameters and association group members with the physical device. Request\n *  Firmware Metadata, Manufacturer-Specific, and Version reports.\n *\n *  Note: Weirdly, update() seems to be called twice. So execution is aborted if there was a previous execution\n *  within two seconds. See: https://community.smartthings.com/t/updated-being-called-twice/62912\n **/\ndef updated() {\n    logger(\"updated()\",\"trace\")\n\n    def cmds = []\n\n    if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {\n        state.updatedLastRanAt = now()\n\n        // Update internal state:\n        state.loggingLevelIDE     = settings.configLoggingLevelIDE.toInteger()\n        state.loggingLevelDevice  = settings.configLoggingLevelDevice.toInteger()\n        state.syncAll             = (\"true\" == settings.configSyncAll)\n        state.proactiveReports    = (\"true\" == settings.configProactiveReports)\n\n        // Manage Schedules:\n        manageSchedules()\n\n        // Update Parameter target values:\n        getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n            state.\"paramTarget${it.id}\" = settings.\"configParam${it.id}\"?.toInteger()\n        }\n\n        // Check if auto-calibration is being forced. If so, must ignore target values for P1/2/30:\n        if (state.paramTarget13 > 0) {\n            state.paramCache13 = null // Remove cached value to force sync of P13:\n            logger(\"Auto-calibration is being forced.\",\"info\")\n            if (state.paramTarget1 != null) logger(\"Auto-calibration is being forced, but a value has been \" +\n            \"provided for parameter #1. This will be ignored! Check Live Logging for the auto-calibrated \" +\n            \"value shortly.\",\"warn\")\n            if (state.paramTarget2 != null) logger(\"Auto-calibration is being forced, but a value has been \" +\n            \"provided for parameter #2. This will be ignored! Check Live Logging for the auto-calibrated \" +\n            \"value shortly.\",\"warn\")\n            if (state.paramTarget30 != null) logger(\"Auto-calibration is being forced, but a value has been \" +\n            \"provided for parameter #30. This will be ignored! Check Live Logging for the auto-calibrated \" +\n            \"value shortly.\",\"warn\")\n            state.paramTarget1 = null\n            state.paramTarget2 = null\n            state.paramTarget30 = null\n        }\n\n        // Update Assoc Group target values:\n        state.assocGroupTarget1 = [ zwaveHubNodeId ] // Assoc Group #1 is Lifeline and will contain controller only.\n        getAssocGroupsMd().findAll( { it.id != 1} ).each {\n            state.\"assocGroupTarget${it.id}\" = parseAssocGroupInput(settings.\"configAssocGroup${it.id}\", it.maxNodes)\n        }\n\n        // Update Protection target values:\n        state.protectLocalTarget = settings.configProtectLocal.toInteger()\n        state.protectRFTarget    = settings.configProtectRF.toInteger()\n\n        // Sync configuration with phyiscal device:\n        sync(state.syncAll)\n\n        // Set target for parameter #13 [Force Auto-calibration] back to 0 [Readout].\n        // Sync will now only complete when auto-calibration has completed:\n        state.paramTarget13 = 0\n\n        // Request device medadata (this just seems the best place to do it):\n        cmds << zwave.firmwareUpdateMdV2.firmwareMdGet()\n        cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet()\n        cmds << zwave.powerlevelV1.powerlevelGet()\n        cmds << zwave.versionV1.versionGet()\n\n        return response(secureSequence(cmds))\n    }\n    else {\n        logger(\"updated(): Ran within last 2 seconds so aborting.\",\"debug\")\n    }\n}\n\n/*****************************************************************************************************************\n *  Private Helper Functions:\n *****************************************************************************************************************/\n\n/**\n *  logger()\n *\n *  Wrapper function for all logging:\n *    Logs messages to the IDE (Live Logging), and also keeps a historical log of critical error and warning\n *    messages by sending events for the device's logMessage attribute.\n *    Configured using configLoggingLevelIDE and configLoggingLevelDevice preferences.\n **/\nprivate logger(msg, level = \"debug\") {\n\n    switch(level) {\n        case \"error\":\n            if (state.loggingLevelIDE >= 1) log.error msg\n            if (state.loggingLevelDevice >= 1) sendEvent(name: \"logMessage\", value: \"ERROR: ${msg}\", displayed: false, isStateChange: true)\n            break\n\n        case \"warn\":\n            if (state.loggingLevelIDE >= 2) log.warn msg\n            if (state.loggingLevelDevice >= 2) sendEvent(name: \"logMessage\", value: \"WARNING: ${msg}\", displayed: false, isStateChange: true)\n            break\n\n        case \"info\":\n            if (state.loggingLevelIDE >= 3) log.info msg\n            break\n\n        case \"debug\":\n            if (state.loggingLevelIDE >= 4) log.debug msg\n            break\n\n        case \"trace\":\n            if (state.loggingLevelIDE >= 5) log.trace msg\n            break\n\n        default:\n            log.debug msg\n            break\n    }\n}\n\n/**\n *  parseAssocGroupInput(string, maxNodes)\n *\n *  Converts a comma-delimited string of destinations (nodes and endpoints) into an array suitable for passing to\n *  multiChannelAssociationSet(). All numbers are interpreted as hexadecimal. Anything that's not a valid node or\n *  endpoint is discarded (warn). If the list has more than maxNodes, the extras are discarded (warn).\n *\n *  Example input strings:\n *    \"9,A1\"      = Nodes: 9 & 161 (no multi-channel endpoints)            => Output: [9, 161]\n *    \"7,8:1,8:2\" = Nodes: 7, Endpoints: Node8:endpoint1 & node8:endpoint2 => Output: [7, 0, 8, 1, 8, 2]\n */\nprivate parseAssocGroupInput(string, maxNodes) {\n    logger(\"parseAssocGroupInput(): Parsing Association Group Nodes: ${string}\",\"trace\")\n\n    // First split into nodes and endpoints. Count valid entries as we go.\n    if (string) {\n        def nodeList = string.split(',')\n        def nodes = []\n        def endpoints = []\n        def count = 0\n\n        nodeList = nodeList.each { node ->\n            node = node.trim()\n            if ( count >= maxNodes) {\n                logger(\"parseAssocGroupInput(): Number of nodes and endpoints is greater than ${maxNodes}! The following node was discarded: ${node}\",\"warn\")\n            }\n            else if (node.matches(\"\\\\p{XDigit}+\")) { // There's only hexadecimal digits = nodeId\n                def nodeId = Integer.parseInt(node,16)  // Parse as hex\n                if ( (nodeId > 0) & (nodeId < 256) ) { // It's a valid nodeId\n                    nodes << nodeId\n                    count++\n                }\n                else {\n                    logger(\"parseAssocGroupInput(): Invalid nodeId: ${node}\",\"warn\")\n                }\n            }\n            else if (node.matches(\"\\\\p{XDigit}+:\\\\p{XDigit}+\")) { // endpoint e.g. \"0A:2\"\n                def endpoint = node.split(\":\")\n                def nodeId = Integer.parseInt(endpoint[0],16) // Parse as hex\n                def endpointId = Integer.parseInt(endpoint[1],16) // Parse as hex\n                if ( (nodeId > 0) & (nodeId < 256) & (endpointId > 0) & (endpointId < 256) ) { // It's a valid endpoint\n                    endpoints.addAll([nodeId,endpointId])\n                    count++\n                }\n                else {\n                    logger(\"parseAssocGroupInput(): Invalid endpoint: ${node}\",\"warn\")\n                }\n            }\n            else {\n                logger(\"parseAssocGroupInput(): Invalid nodeId: ${node}\",\"warn\")\n            }\n        }\n\n        return (endpoints) ? nodes + [0] + endpoints : nodes\n    }\n    else {\n        return []\n    }\n}\n\n/**\n *  sync()\n *\n *  Manages synchronisation of parameters, association groups, and protection state with the physical device.\n *  The syncPending attribute advertises remaining number of sync operations.\n *\n *  Does not return a list of commands, it sends them immediately using sendSecureSequence(). This is required if\n *  triggered by schedule().\n *\n *  Parameters:\n *   forceAll    Force all items to be synced, otherwise only changed items will be synced.\n **/\nprivate sync(forceAll = false) {\n    logger(\"sync(): Syncing configuration with the physical device.\",\"info\")\n\n    def cmds = []\n    def syncPending = 0\n\n    if (forceAll) { // Clear all cached values.\n        getParamsMd().findAll( {!it.readonly} ).each { state.\"paramCache${it.id}\" = null }\n        getAssocGroupsMd().each { state.\"assocGroupCache${it.id}\" = null }\n        state.protectLocalCache = null\n        state.protectRFCache = null\n    }\n\n    getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n        if ( (state.\"paramTarget${it.id}\" != null) & (state.\"paramCache${it.id}\" != state.\"paramTarget${it.id}\") ) {\n            cmds << zwave.configurationV1.configurationSet(parameterNumber: it.id, size: it.size, scaledConfigurationValue: state.\"paramTarget${it.id}\".toInteger())\n            cmds << zwave.configurationV1.configurationGet(parameterNumber: it.id)\n            logger(\"sync(): Syncing parameter #${it.id} [${it.name}]: New Value: \" + state.\"paramTarget${it.id}\",\"info\")\n            syncPending++\n            }\n    }\n\n    getAssocGroupsMd().each {\n        def cachedNodes = state.\"assocGroupCache${it.id}\"\n        def targetNodes = state.\"assocGroupTarget${it.id}\"\n\n        if ( cachedNodes != targetNodes ) {\n            // Display to user in hex format (same as IDE):\n            def targetNodesHex  = []\n            targetNodes.each { targetNodesHex.add(String.format(\"%02X\", it)) }\n            logger(\"sync(): Syncing Association Group #${it.id}: Destinations: ${targetNodesHex}\",\"info\")\n\n            cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: it.id, nodeId: []) // Remove All\n            cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: it.id, nodeId: targetNodes)\n            cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: it.id)\n            syncPending++\n        }\n    }\n\n    if ( (state.protectLocalTarget != null) & (state.protectRFTarget != null)\n      & ( (state.protectLocalCache != state.protectLocalTarget) || (state.protectRFCache != state.protectRFTarget) ) ) {\n\n        logger(\"sync(): Syncing Protection State: Local Protection: ${state.protectLocalTarget}, RF Protection: ${state.protectRFTarget}\",\"info\")\n        cmds << zwave.protectionV2.protectionSet(localProtectionState : state.protectLocalTarget, rfProtectionState: state.protectRFTarget)\n        cmds << zwave.protectionV2.protectionGet()\n        syncPending++\n    }\n\n    sendEvent(name: \"syncPending\", value: syncPending, displayed: false)\n    sendSecureSequence(cmds,1000) // Need a delay of at least 1000ms.\n}\n\n/**\n *  updateSyncPending()\n *\n *  Updates syncPending attribute, which advertises remaining number of sync operations.\n **/\nprivate updateSyncPending() {\n\n    def syncPending = 0\n\n    getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n        if ( (state.\"paramTarget${it.id}\" != null) & (state.\"paramCache${it.id}\" != state.\"paramTarget${it.id}\") ) {\n            syncPending++\n        }\n    }\n\n    getAssocGroupsMd().each {\n        def cachedNodes = state.\"assocGroupCache${it.id}\"\n        def targetNodes = state.\"assocGroupTarget${it.id}\"\n\n        if ( cachedNodes != targetNodes ) {\n            syncPending++\n        }\n    }\n\n    if ( (state.protectLocalCache == null) || (state.protectRFCache == null) ||\n         (state.protectLocalCache != state.protectLocalTarget) || (state.protectRFCache != state.protectRFTarget) ) {\n        syncPending++\n    }\n\n    logger(\"updateSyncPending(): syncPending: ${syncPending}\", \"debug\")\n    if ((syncPending == 0) & (device.latestValue(\"syncPending\") > 0)) logger(\"Sync Complete.\", \"info\")\n    sendEvent(name: \"syncPending\", value: syncPending, displayed: false)\n}\n\n/**\n *  refreshConfig()\n *\n *  Request configuration reports from the physical device: [ Configuration, Association, Protection,\n *   SecuritySupportedCommands, Powerlevel, Manufacturer-Specific, Firmware Metadata, Version, etc. ]\n *\n *  Really only needed at installation or when debugging, as sync will request the necessary reports when the\n *  configuration is changed.\n */\nprivate refreshConfig() {\n    logger(\"refreshConfig()\",\"trace\")\n\n    def cmds = []\n\n    getParamsMd().each { cmds << zwave.configurationV1.configurationGet(parameterNumber: it.id) }\n    getAssocGroupsMd().each { cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: it.id) }\n\n    cmds << zwave.protectionV2.protectionGet()\n    cmds << zwave.securityV1.securityCommandsSupportedGet()\n    cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet()\n    cmds << zwave.firmwareUpdateMdV2.firmwareMdGet()\n    cmds << zwave.versionV1.versionGet()\n    cmds << zwave.powerlevelV1.powerlevelGet()\n\n    sendSecureSequence(cmds, 1000) // Delay must be at least 1000 to reliabilty get all results processed.\n}\n\n/**\n *  secure(cmd)\n *\n *  Secures and formats a command using securityMessageEncapsulation.\n *\n *  Note: All commands are secured, there is little benefit to not securing commands that are not in\n *  state.secureCommandClasses.\n **/\nprivate secure(physicalgraph.zwave.Command cmd) {\n    //if ( state.secureCommandClasses.contains(cmd.commandClassId.toInteger()) ) {...\n    return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()\n}\n\n/**\n *  secureSequence()\n *\n *  Secure an array of commands. Returns a list of formatted commands.\n **/\nprivate secureSequence(commands, delay = 200) {\n    return delayBetween(commands.collect{ secure(it) }, delay)\n}\n\n/**\n *  sendSecureSequence()\n *\n *  Secure an array of commands and send them using sendHubCommand.\n **/\nprivate sendSecureSequence(commands, delay = 200) {\n    sendHubCommand(commands.collect{ response(secure(it)) }, delay)\n}\n\n/**\n *  generatePrefsParams()\n *\n *  Generates preferences (settings) for device parameters.\n **/\nprivate generatePrefsParams() {\n        section {\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"DEVICE PARAMETERS:\",\n                description: \"Device parameters are used to customise the physical device. \" +\n                             \"Refer to the product documentation for a full description of each parameter.\"\n            )\n\n    getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n\n        def lb = (it.description.length() > 0) ? \"\\n\" : \"\"\n\n        switch(it.type) {\n            case \"number\":\n            input (\n                name: \"configParam${it.id}\",\n                title: \"#${it.id}: ${it.name}: \\n\" + it.description + lb +\"Default Value: ${it.defaultValue}\",\n                type: it.type,\n                range: it.range,\n//                defaultValue: it.defaultValue, // iPhone users can uncomment these lines!\n                required: it.required\n            )\n            break\n\n            case \"enum\":\n            input (\n                name: \"configParam${it.id}\",\n                title: \"#${it.id}: ${it.name}: \\n\" + it.description + lb + \"Default Value: ${it.defaultValue}\",\n                type: it.type,\n                options: it.options,\n//                defaultValue: it.defaultValue, // iPhone users can uncomment these lines!\n                required: it.required\n            )\n            break\n        }\n    }\n        } // section\n}\n\n/**\n *  generatePrefsAssocGroups()\n *\n *  Generates preferences (settings) for Association Groups.\n **/\nprivate generatePrefsAssocGroups() {\n        section {\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"ASSOCIATION GROUPS:\",\n                description: \"Association groups enable the dimmer to control other Z-Wave devices directly, \" +\n                             \"without participation of the main controller.\\n\" +\n                             \"Enter a comma-delimited list of destinations (node IDs and/or endpoint IDs) for \" +\n                             \"each association group. All IDs must be in hexadecimal format. E.g.:\\n\" +\n                             \"Node destinations: '11, 0F'\\n\" +\n                             \"Endpoint destinations: '1C:1, 1C:2'\"\n            )\n\n    getAssocGroupsMd().findAll( { it.id != 1} ).each { // Don't show AssocGroup1 (Lifeline).\n            input (\n                name: \"configAssocGroup${it.id}\",\n                title: \"Association Group #${it.id}: ${it.name}: \\n\" + it.description + \" \\n[MAX NODES: ${it.maxNodes}]\",\n                type: \"text\",\n//                defaultValue: \"\", // iPhone users can uncomment these lines!\n                required: false\n            )\n        }\n    }\n}\n\n/**\n *  manageSchedules()\n *\n *  Schedules/unschedules Nightmode.\n **/\nprivate manageSchedules() {\n    logger(\"manageSchedules()\",\"trace\")\n\n    if (configNightmodeStartTime) {\n        schedule(configNightmodeStartTime, enableNightmode)\n        logger(\"manageSchedules(): Nightmode scheduled to start at ${configNightmodeStartTime}\",\"debug\")\n    } else {\n        try {\n            unschedule(\"enableNightmode\")\n        }\n        catch(e) {\n            // Unschedule failed\n        }\n    }\n\n    if (configNightmodeStopTime) {\n        schedule(configNightmodeStopTime, disableNightmode)\n        logger(\"manageSchedules(): Nightmode scheduled to stop at ${configNightmodeStopTime}\",\"debug\")\n    } else {\n        try {\n            unschedule(\"disableNightmode\")\n        }\n        catch(e) {\n            // Unschedule failed\n        }\n    }\n\n}\n\n/**\n *  test()\n *\n *  Temp testing method. Called from 'test' tile.\n **/\nprivate test() {\n    logger(\"test()\",\"trace\")\n\n    def cmds = []\n\n    if (cmds) return secureSequence(cmds,200)\n}\n\n/*****************************************************************************************************************\n *  Static Matadata Functions:\n *\n *  These functions encapsulate metadata about the device. Mostly obtained from:\n *   Z-wave Alliance Reference for Fibaro Dimmer 2: http://products.z-wavealliance.org/products/1729\n *****************************************************************************************************************/\n\n/**\n *  getCommandClassVersions()\n *\n *  Returns a map of the command class versions supported by the device. Used by parse() and zwaveEvent() to\n *  extract encapsulated commands from MultiChannelCmdEncap, MultiInstanceCmdEncap, SecurityMessageEncapsulation,\n *  and Crc16Encap messages.\n *\n *  Reference: http://products.z-wavealliance.org/products/1729/classes\n **/\nprivate getCommandClassVersions() {\n    return [0x20: 1, // Basic V1\n            0x22: 1, // Application Status V1\n            0x26: 3, // Switch Multilevel V3\n            0x27: 1, // Switch All V1\n            0x2B: 1, // Scene Activation V1\n            0x31: 4, // Sensor Multilevel V4\n            0x32: 3, // Meter V3\n            0x56: 1, // CRC16 Encapsulation V1\n            0x59: 1, // Association Group Information V1 (Not handled, as no need)\n            0x5A: 1, // Device Reset Locally V1\n            //0x5E: 2, // Z-Wave Plus Info V2 (Not supported by SmartThings)\n            0x60: 3, // Multi Channel V4 (Device supports V4, but SmartThings only supports V3)\n            0x70: 1, // Configuration V1\n            0x71: 3, // Notification V5 ((Device supports V5, but SmartThings only supports V3)\n            0x72: 2, // Manufacturer Specific V2\n            0x73: 1, // Powerlevel V1\n            0x75: 2, // Protection V2\n            0x7A: 2, // Firmware Update MD V3 (Device supports V3, but SmartThings only supports V2)\n            0x85: 2, // Association V2\n            0x86: 1, // Version V2 (Device supports V2, but SmartThings only supports V1)\n            0x8E: 2, // Multi Channel Association V3 (Device supports V3, but SmartThings only supports V2)\n            0x98: 1  // Security V1\n           ]\n}\n\n/**\n *  getParamsMd()\n *\n *  Returns device parameters metadata. Used by sync(), updateSyncPending(),  and generatePrefsParams().\n *\n *  Reference: http://products.z-wavealliance.org/products/1729/configs\n **/\nprivate getParamsMd() {\n    return [\n        [id:  1, size: 1, type: \"number\", range: \"1..98\", defaultValue: 1, required: false, readonly: false,\n         name: \"Minimum Brightness Level\",\n         description: \"Set automatically during the calibration process, but can be changed afterwards.\\n\" +\n         \"Values: 1-98 = Brightness level (%)\"],\n        [id:  2, size: 1, type: \"number\", range: \"2..99\", defaultValue: 99, required: false, readonly: false,\n         name: \"Maximum Brightness Level\",\n         description: \"Set automatically during the calibration process, but can be changed afterwards.\\n\" +\n         \"Values: 2-99 = Brightness level (%)\"],\n        [id:  3, size: 1, type: \"number\", range: \"1..99\", defaultValue: 1, required: false, readonly: false,\n         name: \"Incandescence Level of CFLs\",\n         description : \"The Dimmer 2 will set to this value after first switch on. It is required for warming up \" +\n         \"and switching dimmable compact fluorescent lamps and certain types of light sources.\\n\" +\n         \"Values: 1-99 = Brightness level (%)\"],\n        [id:  4, size: 2, type: \"number\", range: \"0..255\", defaultValue: 0, required: false, readonly: false,\n         name: \"Incandescence Time of CFLs\",\n         description : \"The time required for switching compact fluorescent lamps and certain types of light sources.\\n\" +\n         \"Values:\\n0 = Function Disabled\\n1-255 = 0.1-25.5s in 0.1s steps\"],\n        [id:  5, size: 1, type: \"number\", range: \"1..99\", defaultValue : 1, required: false, readonly: false,\n         name: \"Dimming Step Size (Auto)\",\n         description : \"The percentage value of a dimming step during automatic control.\\n\" +\n         \"Values: 1-99 = Dimming step (%)\"],\n        [id:  6, size: 2, type: \"number\", range: \"0..255\", defaultValue: 1, required: false, readonly: false,\n         name: \"Dimming Step Time (Auto)\",\n         description : \"The time of a single dimming step during automatic control.\\n\" +\n         \"Values: 0-255 = 0-2.55s, in 10ms steps\"],\n        [id:  7, size: 1, type: \"number\", range: \"1..99\", defaultValue: 1, required: false, readonly: false,\n         name: \"Dimming Step Size (Manual)\",\n         description : \"The percentage value of a dimming step during manual control.\\n\" +\n         \"Values: 1-99 = Dimming step (%)\"],\n        [id:  8, size: 2, type: \"number\", range: \"0..255\", defaultValue: 5, required: false, readonly: false,\n         name: \"Dimming Step Time (Manual)\",\n         description : \"The time of a single dimming step during manual control.\\n\" +\n         \"Values: 0-255 = 0-2.55s, in 10ms steps\"],\n        [id:  9, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"State After Power Failure\",\n         description : \"Dimmer state to restore after a power failure.\",\n         options: [\"0\" : \"0: Off\", \"1\" : \"1: Restore Previous State\"] ],\n        [id: 10, size: 2, type: \"number\", range: \"0..32767\", defaultValue: 0, required: false, readonly: false,\n         name: \"Timer Functionality (Auto-off)\",\n         description : \"Automatically switch off the device after a specified time.\\n\" +\n         \"Values:\\n0 = Function Disabled\\n1-32767 = time in seconds\"],\n        [id: 11, size: 2, type: \"enum\", defaultValue: \"255\", required: false, readonly: false,\n         name: \"ALL ON/ALL OFF Function\",\n         description : \"Response to SWITCH_ALL_SET commands.\",\n         options: [\"0\" : \"0: All ON not active, All OFF not active\",\n                   \"1\" : \"1: All ON not active, All OFF active\",\n                   \"2\" : \"2: All ON active, All OFF not active\",\n                   \"255\" : \"255: All ON active, All OFF active\"] ],\n        [id: 13, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"Force Auto-calibration\",\n         description : \"During calibration this parameter is set to 1 or 2 and switched to 0 upon completion.\",\n         options: [\"0\" : \"0: Readout\",\n                   \"1\" : \"1: Force auto-calibration WITHOUT Fibaro Bypass 2\",\n                   \"2\" : \"2: Force auto-calibration WITH Fibaro Bypass 2\"] ],\n        [id: 14, size: 1, type: \"readonly\", readonly: true,\n         name: \"Auto-calibration Status\",\n         description : \"Read-Only: Indicates if dimmer is using auto-calibration (1) or manual (0) settings.\"],\n        [id: 15, size: 1, type: \"number\", range: \"0..99\", defaultValue: 30, required: false, readonly: false,\n         name: \"Burnt Out Bulb Detection\",\n         description : \"Power variation, compared to standard power consumption (measured during calibration), \" +\n         \"to be interpreted as load error/burnt out bulb.\\n\" +\n         \"Values:\\n0 = Function Disabled\\n1-99 = Power variation (%)\"],\n        [id: 16, size: 2, type: \"number\", range: \"0..255\", defaultValue: 5, required: false, readonly: false,\n         name: \"Time Delay for Burnt Out Bulb/Overload Detection\",\n         description : \"Time delay (in seconds) for LOAD ERROR or OVERLOAD detection.\\n\" +\n         \"Values:\\n0 = Detection Disabled\\n1-255 = Time delay (s)\"],\n        [id: 19, size: 1, type: \"number\", range: \"0..99\", defaultValue: 0, required: false, readonly: false,\n         name: \"Forced Switch-on Brightness Level\",\n         description : \"Switching on the dimmer will always set this brightness level.\\n\" +\n         \"Note, the Nightmode feature can be used to change this parameter on a schedule.\\n\" +\n         \"Values:\\n0 = Function Disabled\\n1-99 = Brightness level (%)\"],\n        [id: 20, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"Switch Type\",\n         description : \"Physical switch type: momentary, toggle, or roller blind (S1 to brighten, S2 to dim).\",\n         options: [\"0\" : \"0: Momentary Switch\",\n                   \"1\" : \"1: Toggle Switch\",\n                   \"2\" : \"2: Roller Blind Switch\"] ],\n        [id: 21, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"Value Sent to Associated Devices on Single Click\",\n         description : \"0xFF will set associated devices to their last-saved state. Current Level will \" +\n         \"synchronise the state of all devices with this dimmer.\",\n         options: [\"0\" : \"0: 0xFF\",\n                   \"1\" : \"1: Current Level\"] ],\n        [id: 22, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"Assign Toggle Switch Status to Device Status\",\n         description : \"By default, each change of toggle switch position results in an on/off action \" +\n         \"regardless the physical connection of contacts.\",\n         options: [\"0\" : \"0: Change on Every Switch State Change\",\n                   \"1\" : \"1: Synchronise with Switch State\"] ],\n        [id: 23, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"Double-click sets Max Brightness\",\n         description : \"Double-clicking will set brightness level to maximum.\",\n         options: [\"0\" : \"0: Double-click DISABLED\",\n                   \"1\" : \"1: Double-click ENABLED\"] ],\n        [id: 24, size: 1, type: \"number\", range: \"0..31\", defaultValue: 0, required: false, readonly: false,\n         name: \"Command Frames Sent to 2nd and 3rd Association Groups (S1 Associations)\",\n         description : \"Determines which actions will not result in sending frames to association groups.\\n\" +\n         \"Values (add together):\\n\" +\n         \"0 = All actions sent to association groups\\n\" +\n         \"1 = Do not send when switching ON (single click)\\n\" +\n         \"2 = Do not send when switching OFF (single click)\\n\" +\n         \"4 = Do not send when changing dimming level (holding and releasing)\\n\" +\n         \"8 = Do not send on double click\\n\" +\n         \"16 = Send 0xFF value on double click\"],\n        [id: 25, size: 1, type: \"number\", range: \"0..31\", defaultValue: 0, required: false, readonly: false,\n         name: \"Command Frames Sent to 4th and 5th Association Groups (S2 Associations)\",\n         description : \"Determines which actions will not result in sending frames to association groups.\\n\" +\n         \"Values (add together):\\n\" +\n         \"0 = All actions sent to association groups\\n\" +\n         \"1 = Do not send when switching ON (single click)\\n\" +\n         \"2 = Do not send when switching OFF (single click)\\n\" +\n         \"4 = Do not send when changing dimming level (holding and releasing)\\n\" +\n         \"8 = Do not send on double click\\n\" +\n         \"16 = Send 0xFF value on double click\"],\n        [id: 26, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"3-way Switch Function\",\n         description : \"Switch S2 also controls the dimmer when in 3-way switch mode. \" +\n         \"Function is disabled if parameter #20 is set to 2 (roller blind switch).\",\n         options: [\"0\" : \"0: 3-way switch function for S2 DISABLED\",\n                   \"1\" : \"1: 3-way switch function for S2 ENABLED\"] ],\n        [id: 27, size: 1, type: \"number\", range: \"0..15\", defaultValue: 15, required: false, readonly: false,\n         name: \"Association Group Security Mode\",\n         description : \"Defines if commands sent to association groups are secure or non-secure.\\n\" +\n         \"Values (add together):\\n\" +\n         \"0 = all groups (2-5) sent as non-secure\\n\" +\n         \"1 = 2nd group sent as secure\\n\" +\n         \"2 = 3rd group sent as secure\\n\" +\n         \"4 = 4th group sent as secure\\n\" +\n         \"8 = 5th group sent as secure\\n\" +\n         \"E.g. 15 = all groups (2-5) sent as secure.\"],\n        [id: 28, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"Scene Activation\",\n         description : \"Defines if SCENE_ACTIVATION_SET commands are sent.\",\n         options: [\"0\" : \"0: Function DISABLED\",\n                   \"1\" : \"1: Function ENABLED\"] ],\n        [id: 29, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"Swap S1 and S2\",\n         description : \"Swap the roles of S1 and S2 without changes to physical wiring.\",\n         options: [\"0\" : \"0: Standard Mode\",\n                   \"1\" : \"1: S1 operates as S2, S2 operates as S1\"] ],\n        [id: 30, size: 1, type: \"enum\", defaultValue: \"2\", required: false, readonly: false,\n         name: \"Load Control Mode\",\n         description : \"Override the dimmer mode (i.e. leading or trailing edge).\",\n         options: [\"0\" : \"0: Force leading edge mode\",\n                   \"1\" : \"1: Force trailing edge mode\",\n                   \"2\" : \"2: Automatic (based on auto-calibration)\"] ],\n        [id: 31, size: 1, type: \"readonly\", readonly: true,\n         name: \"Load Control Mode Recognised During Auto-calibration\",\n         description : \"Read-Only: Indicates the load control mode recognised during auto-calibration. Leading Edge (0) / trailing Edge (1).\"],\n        [id: 32, size: 1, type: \"enum\", defaultValue: \"2\", required: false, readonly: false,\n         name: \"On/Off Mode\",\n         description : \"This mode is necessary when connecting non-dimmable light sources.\",\n         options: [\"0\" : \"0: On/Off mode DISABLED (dimming is possible)\",\n                   \"1\" : \"1: On/Off mode ENABLED (dimming not possible)\",\n                   \"2\" : \"2: Automatic (based on auto-calibration)\"] ],\n        [id: 33, size: 1, type: \"readonly\", readonly: true,\n         name: \"Dimmability of the Load\",\n         description : \"Read-Only: Indicates the dimmability of the load recognised during auto-calibration. Dimmable (0) / Non-dimmable (1).\"],\n        [id: 34, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"Soft-Start\",\n         description : \"Time required to warm up the filament of halogen bulbs.\",\n         options: [\"0\" : \"0: No soft-start\",\n                   \"1\" : \"1: Short soft-start (0.1s)\",\n                   \"2\" : \"2: Long soft-start (0.5s)\"] ],\n        [id: 35, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"Auto-calibration\",\n         description : \"Determines when auto-calibration is triggered.\",\n         options: [\"0\" : \"0: No auto-calibration\",\n                   \"1\" : \"1: Auto-calibration after first power on only\",\n                   \"2\" : \"2: Auto-calibration after each power on\",\n                   \"3\" : \"3: Auto-calibration after first power on and after each LOAD ERROR\",\n                   \"4\" : \"4: Auto-calibration after each power on and after each LOAD ERROR\"] ],\n        [id: 37, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"Behaviour After OVERCURRENT or SURGE\",\n         description : \"The dimmer will turn off when a surge or overcurrent is detected. \" +\n         \"By default, the device performs three attempts to turn on the load.\",\n         options: [\"0\" : \"0: Device disabled until command or external switch\",\n                   \"1\" : \"1: Three attempts to turn on the load\"] ],\n        [id: 39, size: 2, type: \"number\", range: \"0..350\", defaultValue : 250, required: false, readonly: false,\n         name: \"Power Limit - OVERLOAD\",\n         description : \"Reaching the defined value will result in turning off the load. \" +\n         \"Additional apparent power limit of 350VA is active by default.\\n\" +\n         \"Values:\\n0 = Function Disabled\\n1-350 = Power limit (W)\"],\n        [id: 40, size: 1, type: \"enum\", defaultValue: \"3\", required: false, readonly: false,\n         name: \"Response to General Purpose Alarm\",\n         description : \"\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Turn on the load\",\n                   \"2\" : \"2: Turn off the load\",\n                   \"3\" : \"3: Load blinking\"] ],\n        [id: 41, size: 1, type: \"enum\", defaultValue: \"2\", required: false, readonly: false,\n         name: \"Response to Water Flooding Alarm\",\n         description : \"\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Turn on the load\",\n                   \"2\" : \"2: Turn off the load\",\n                   \"3\" : \"3: Load blinking\"] ],\n        [id: 42, size: 1, type: \"enum\", defaultValue: \"3\", required: false, readonly: false,\n         name: \"Response to Smoke, CO, or CO2 Alarm\",\n         description : \"\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Turn on the load\",\n                   \"2\" : \"2: Turn off the load\",\n                   \"3\" : \"3: Load blinking\"] ],\n        [id: 43, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \" Response to Temperature Alarm\",\n         description : \"\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Turn on the load\",\n                   \"2\" : \"2: Turn off the load\",\n                   \"3\" : \"3: Load blinking\"] ],\n        [id: 44, size: 2, type: \"number\", range: \"1..32767\", defaultValue : 600, required: false, readonly: false,\n         name: \"Time of Alarm State\",\n         description : \"Values: 1-32767 = Time (s)\"],\n        [id: 45, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"OVERLOAD Alarm Report\",\n         description : \"Power consumption above Power Limit.\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Send an alarm frame\"] ],\n        [id: 46, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"LOAD ERROR Alarm Report\",\n         description : \"No load, load failure, or burnt out bulb.\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Send an alarm frame\"] ],\n        [id: 47, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"OVERCURRENT Alarm Report\",\n         description : \"Short circuit, or burnt out bulb causing overcurrent\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Send an alarm frame\"] ],\n        [id: 48, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"SURGE Alarm Report\",\n         description : \"\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Send an alarm frame\"] ],\n        [id: 49, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         name: \"OVERHEAT and VOLTAGE DROP Alarm Report\",\n         description : \"Critical temperature, or low voltage.\",\n         options: [\"0\" : \"0: No reaction\",\n                   \"1\" : \"1: Send an alarm frame\"] ],\n        [id: 50, size: 1, type: \"number\", range: \"0..100\", defaultValue : 10, required: false, readonly: false,\n         name: \"Power Reports Threshold\",\n         description : \"Power level change that will result in a new power report being sent.\\n\" +\n         \"Values:\\n0 = Reports disabled\\n1-100 = % change from previous report\"],\n        [id: 52, size: 2, type: \"number\", range: \"0..32767\", defaultValue : 3600, required: false, readonly: false,\n         name: \"Reporting Period\",\n         description : \"The time period between consecutive power and energy reports.\\n\" +\n         \"Values:\\n0 = Reports disabled\\n1-32767 = Time period (s)\"],\n        [id: 53, size: 2, type: \"number\", range: \"0..255\", defaultValue : 10, required: false, readonly: false,\n         name: \"Energy Reports Threshold\",\n         description : \"Energy level change that will result in a new energy report being sent.\\n\" +\n         \"Values:\\n0 = Reports disabled,\\n1-255 = 0.01-2.55 kWh\"],\n        [id: 54, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"Self-measurement\",\n         description : \"Include power and energy consumed by the device itself in reports.\",\n         options: [\"0\" : \"0: Self-measurement DISABLED\",\n                   \"1\" : \"1: Self-measurement ENABLED\"] ],\n        [id: 58, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         name: \"Method of Calculating Active Power\",\n         description : \"Useful in 2-wire configurations with non-resistive loads.\",\n         options: [\"0\" : \"0: Standard algorithm\",\n                   \"1\" : \"1: Based on calibration data\",\n                   \"2\" : \"2: Based on control angle\"] ],\n        [id: 59, size: 2, type: \"number\", range: \"0..500\", defaultValue : 0, required: false, readonly: false,\n         name: \"Approximated Power at Max Brightness\",\n         description : \"Determines the approximate value of the power that will be reported by the device at \" +\n         \"it's maximum brightness level.\\n\" +\n         \"Values: 0-500 = Power (W)\"],\n    ]\n}\n\n/**\n *  getAssocGroupsMd()\n *\n *  Returns association groups metadata. Used by sync(), updateSyncPending(), and generatePrefsAssocGroups().\n *\n *  Reference: http://products.z-wavealliance.org/products/1729/assoc\n **/\nprivate getAssocGroupsMd() {\n    return [\n        [id:  1, maxNodes: 1, name: \"Lifeline\",\n         description : \"Reports device state. Main Z-Wave controller should be added to this group.\"],\n        [id:  2, maxNodes: 8, name: \"On/Off (S1)\",\n         description : \"Sends on/off commands to associated devices when S1 is pressed (BASIC_SET).\"],\n        [id:  3, maxNodes: 8, name: \"Dimmer (S1)\",\n         description : \"Sends dim/brighten commands to associated devices when S1 is pressed (SWITCH_MULTILEVEL_SET).\"],\n        [id:  4, maxNodes: 8, name: \"On/Off (S2)\",\n         description : \"Sends on/off commands to associated devices when S2 is pressed (BASIC_SET).\"],\n        [id:  5, maxNodes: 8, name: \"Dimmer (S2)\",\n         description : \"Sends dim/brighten commands to associated devices when S2 is pressed (SWITCH_MULTILEVEL_SET).\"]\n    ]\n}\n"
  },
  {
    "path": "devices/fibaro-flood-sensor/README.md",
    "content": "# Fibaro Flood Sensor (FGFS-101) (EU)\nhttps://github.com/codersaur/SmartThings/tree/master/devices/fibaro-flood-sensor\n\nCopyright (c) [David Lomas](https://github.com/codersaur)\n\n## Overview\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-flood-sensor/screenshots/ffs-ss-tiles-main.png\" width=\"200\" align=\"right\">\nAn advanced SmartThings device handler for the Fibaro Flood Sensor (FGFS-101) (EU).\n\n**The newer ZW5 (Z-Wave Plus) version is NOT supported.**\n\n### Key features:\n* Reports water, temperature, tamper, and battery attributes.\n* All Z-Wave parameters can be configured using the SmartThings GUI.\n* Multi-channel device associations can be configured using the SmartThings GUI.\n* Supports battery and hard-wired power modes.\n* _Sync_ tile indicates when all configuration options are successfully synchronised with the physical device.\n* Logger functionality enables critical errors and warnings to be saved to the _logMessage_ attribute.\n* Extensive inline code comments to support community development.\n\n## Installation\n\n1. Follow [these instructions](https://github.com/codersaur/SmartThings#device-handler-installation-procedure) to install the device handler in the SmartThings IDE.\n\n2. **Note for iPhone users**: The _defaultValue_ of inputs (preferences) are commented out to cater for Android users. iPhone users can uncomment these lines if they wish (search for \"iPhone\" in the code).\n\n3. From the SmartThings app on your phone, edit the device settings to suit your installation and hit _Done_. Note, if the device is in battery-powered mode, new settings will only by synchronised when the device wakes up, however you should be able to give the device a shake to force it to wake up.\n\n## Settings\n\n#### General Settings:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-flood-sensor/screenshots/ffs-ss-settings-general.png\" width=\"200\" align=\"right\">\n\n* **IDE Live Logging Level**: Set the level of log messages shown in the SmartThings IDE _Live Logging_ tab. For normal operation _Info_ or _Warning_ is recommended, if troubleshooting use _Debug_ or _Trace_.\n\n* **Device Logging Level**: Set the level of log messages that will be recorded in the device's _logMessage_ attribute. This offers a way to review historical messages without having to keep the IDE _Live Logging_ screen open. To prevent excessive events, the maximum level supported is _Warning_.\n\n* **Force Full Sync**: By default, only settings that have been modified will be synchronised with the device. Enable this setting to force all device parameters and association groups to be re-sent to the device.\n\n* **Auto-reset Tamper Alarm**: Automatically reset tamper alarms after a time delay.\n\n#### Wake Up Interval:\n\n* **Wake Up Interval**: The device will wake up periodically to sync configuration. A longer interval will save battery power. Only applicable when in battery-power mode.\n\n#### Device Parameters:\n\nThe settings in this section can be used to specify the value of all writable device parameters. It is recommended to consult the [manufacturer's manual](http://manuals.fibaro.com/flood-sensor/) for a full description of each parameter.\n\nIf no value is specified for a parameter, then it will not be synched with the device and the existing value in the device will be preserved.\n\n#### Multi-channel Device Associations:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-flood-sensor/screenshots/ffs-ss-settings-assoc.png\" width=\"200\" align=\"right\">\n\nThe Fibaro Floor Sensor supports _Multi-channel_ Device Associations. This allows the device to send water and tamper alarm commands directly to Z-Wave other devices (e.g. sirens), without the commands being processed by the SmartThings hub. This results in faster response times compared to using a SmartApp.\n\nThe Fibaro Flood Sensor supports three association groups:\n\n- **Association Group #1**: Sends BASIC_SET or ALARM commands when the sensor detects water.\n- **Association Group #2**: Sends ALARM commands when the device detects movement or tampering.\n- **Association Group #3**: Device status (contains the main controller only).\n\nThe members of each _Association Group_ must be defined as a comma-delimited list of target nodes. Each target device can be specified in one of two ways:\n- _Node_: A single hexadecimal number (e.g. \"0C\") representing the target _Device Network ID_.\n- _Endpoint_: A pair of hexadecimal numbers separated by a colon (e.g. \"10:1\") that represent the target _Device Network ID_ and _Endpoint ID_ respectively. For devices that support multiple endpoints, this allows a specific endpoint to be targeted by the association group.\n\nYou can find the _Device Network ID_ for all Z-Wave devices in your SmartThings network from the _My Devices_ tab in the SmartThings IDE. Consult the relevant manufacturer's manual for information about the endpoints supported by a particular target device.\n\n## GUI\n\n#### Main Tile:\nThe main tile indicates water detection and temperature.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-flood-sensor/screenshots/ffs-ss-tiles-wet.png\" width=\"200\">\n\n#### Power Status Tile:\nThis tile indicates the battery level, or that the device is hard-wired to DC power.\n\n#### Tamper Tile:\nThis tile indicates if the device has detected movement or tampering. Pressing the tile will clear the tamper status. Tamper alerts can also be cleared automatically (see settings).\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-flood-sensor/screenshots/ffs-ss-tiles-tamper.png\" width=\"200\">\n\n#### Sync Tile:\nThis tile indicates when all configuration settings have been successfully synchronised with the physical device. Note, if the device is in battery-powered mode, new settings will only by synchronised when the device wakes up.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-flood-sensor/screenshots/ffs-ss-tiles-sync.png\" width=\"200\">\n\n## SmartApp Integration\n\n#### Attributes:\n\nThe device handler publishes the following attributes:\n\n* **battery [NUMBER]**: Current battery level (%).\n* **logMessage [STRING]**: Important log messages.\n* **powerSource [ENUM]**: Indicates if the device is battery-, dc-, or mains-powered.\n* **syncPending [NUMBER]**: The number of configuration items that need to be synced with the physical device. _0_ if the device is fully synchronised.\n* **tamper [ENUM]**: Indicates if the device has been tampered with.\n* **temperature [NUMBER]**: Current temperature (C).\n* **water [ENUM]**: Indicates if the sensor is 'dry' or 'wet'.\n\n#### Commands:\n\nThe device exposes the following custom commands which can be called from a SmartApp:\n\n* **resetTamper()**: Clear any tamper alerts.\n\n## Version History\n\n#### 2017-03-02: v1.00\n  *  Initial version.\n\n## Physical Device Notes\n\nGeneral notes concerning the Fibaro Flood Sensor:\n\n* **Remember to calibrate temperature measurements using parameter #73.** The Fibaro Flood Sensor typically reports temperatures that are ~5°C above the air temperature outside the casing.\n* If the device does not send temperature reports with the expected frequency, it is recommended to perform a full reset of the device.\n* In hard-wired power mode, the device is active and listening. It will not issue Wake Up notifications or battery reports.\n* In battery-powered mode, the device is _sleepy_ and can only be configured after it has woken up.\n\n## References\n Some useful links relevant to the development of this device handler:\n* [Fibaro Flood Sensor - Z-Wave certification information](http://products.z-wavealliance.org/products/1036)\n* [Fibaro Flood Sensor  - Manual](http://manuals.fibaro.com/flood-sensor/)\n\n## License\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n"
  },
  {
    "path": "devices/fibaro-flood-sensor/fibaro-flood-sensor.groovy",
    "content": "/*****************************************************************************************************************\n *  Copyright: David Lomas (codersaur)\n *\n *  Name: Fibaro Flood Sensor Advanced\n *\n *  Author: David Lomas (codersaur)\n *\n *  Date: 2017-03-02\n *\n *  Version: 1.00\n *\n *  Source: https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-flood-sensor\n *\n *  Author: David Lomas (codersaur)\n *\n *  Description: An advanced SmartThings device handler for the Fibaro Flood Sensor (FGFS-101) (EU),\n *   with firmware: 2.6 or older.\n *\n *  For full information, including installation instructions, exmples, and version history, see:\n *   https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-flood-sensor\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n *****************************************************************************************************************/\nmetadata {\n    definition (name: \"Fibaro Flood Sensor Advanced\", namespace: \"codersaur\", author: \"David Lomas\") {\n        capability \"Sensor\"\n        capability \"Water Sensor\"\n        capability \"Tamper Alert\"\n        capability \"Temperature Measurement\"\n        capability \"Battery\"\n        capability \"Power Source\"\n\n        // Standard (Capability) Attributes:\n        attribute \"battery\", \"number\"\n        attribute \"powerSource\", \"enum\", [\"battery\", \"dc\", \"mains\", \"unknown\"]\n        attribute \"tamper\", \"enum\", [\"detected\", \"clear\"]\n        attribute \"temperature\", \"number\"\n        attribute \"water\", \"enum\", [\"dry\", \"wet\"]\n\n        // Custom Attributes:\n        attribute \"batteryStatus\", \"string\"     // Indicates DC-power or battery %.\n        attribute \"logMessage\", \"string\"        // Important log messages.\n        attribute \"syncPending\", \"number\"       // Number of config items that need to be synced with the physical device.\n\n        // Custom Commands:\n        command \"resetTamper\"\n        command \"sync\"\n        command \"test\"\n\n        // Fingerprints:\n        fingerprint mfr: \"010F\", prod: \"0B00\", model: \"1001\"\n        fingerprint mfr: \"010F\", prod: \"0B00\", model: \"2001\"\n        fingerprint deviceId: \"0xA102\", inClusters: \"0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84\"\n    }\n\n    tiles(scale: 2) {\n        multiAttributeTile(name:\"multiTile\", type:\"generic\", width:6, height:4) {\n            tileAttribute(\"device.water\", key: \"PRIMARY_CONTROL\") {\n                attributeState \"dry\", label:'', icon:\"st.alarm.water.dry\", backgroundColor:\"#79b821\"\n                attributeState \"wet\", label:'', icon:\"st.alarm.water.wet\", backgroundColor:\"#53a7c0\"\n            }\n            tileAttribute(\"device.temperature\", key: \"SECONDARY_CONTROL\") {\n                attributeState \"temperature\", label:'Temperature: ${currentValue}°C'\n            }\n        }\n\n        standardTile(\"water\", \"device.water\", width: 2, height: 2, canChangeIcon: true) {\n            state \"dry\", icon:\"st.alarm.water.dry\", backgroundColor:\"#ffffff\"\n            state \"wet\", icon:\"st.alarm.water.wet\", backgroundColor:\"#53a7c0\"\n        }\n        valueTile(\"temperature\", \"device.temperature\", width: 2, height: 2) {\n            state \"temperature\", label:'${currentValue}°C'\n        }\n        standardTile(\"tamper\", \"device.tamper\", decoration: \"flat\", width: 2, height: 2) {\n            state(\"default\", label:\"tampered\", icon:\"st.security.alarm.alarm\", backgroundColor:\"#FF6600\", action: \"resetTamper\")\n            state(\"clear\", label:\"clear\", icon:\"st.security.alarm.clear\", backgroundColor:\"#ffffff\")\n        }\n        valueTile(\"battery\", \"device.battery\", width: 2, height: 2, decoration: \"flat\") {\n            state \"battery\", label:'Battery: ${currentValue}%'\n        }\n        standardTile(\"powerSource\", \"device.powerSource\", width: 2, height: 2, decoration: \"flat\") {\n            state \"powerSource\", label:'${currentValue}-Powered'\n        }\n        valueTile(\"batteryStatus\", \"device.batteryStatus\", width: 2, height: 2, decoration: \"flat\", inactiveLabel: false) {\n            state \"batteryStatus\", label:'${currentValue}', unit:\"\"\n        }\n\n        standardTile(\"syncPending\", \"device.syncPending\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Sync Pending', backgroundColor:\"#FF6600\", action:\"sync\"\n            state \"0\", label:'Synced', action:\"\", backgroundColor:\"#79b821\"\n        }\n        standardTile(\"test\", \"device.test\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Test', action:\"test\"\n        }\n\n        main([\"water\",\"temperature\"])\n        details([\n            \"multiTile\",\n            //\"water\", // Also in multiTile.\n            //\"temperature\", // Also in multiTile.\n            //\"battery\",\n            //\"powerSource\",\n            \"batteryStatus\",\n            \"tamper\",\n            \"syncPending\"\n            //,\"test\"\n        ])\n    }\n\n    preferences {\n\n        section { // GENERAL:\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"GENERAL:\",\n                description: \"General device handler settings.\"\n            )\n\n            input (\n                name: \"configLoggingLevelIDE\",\n                title: \"IDE Live Logging Level: Messages with this level and higher will be logged to the IDE.\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"None\",\n                    \"1\" : \"Error\",\n                    \"2\" : \"Warning\",\n                    \"3\" : \"Info\",\n                    \"4\" : \"Debug\",\n                    \"5\" : \"Trace\"\n                ],\n//                defaultValue: \"3\", // iPhone users can uncomment these lines!\n                required: true\n            )\n\n            input (\n                name: \"configLoggingLevelDevice\",\n                title: \"Device Logging Level: Messages with this level and higher will be logged to the logMessage attribute.\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"None\",\n                    \"1\" : \"Error\",\n                    \"2\" : \"Warning\"\n                ],\n//                defaultValue: \"2\", // iPhone users can uncomment these lines!\n                required: true\n            )\n\n            input (\n                name: \"configSyncAll\",\n                title: \"Force Full Sync: All device parameters and association groups will be re-sent to the device. \" +\n                \"This will happen at next wake up or on receipt of an alarm/temperature report.\",\n                type: \"boolean\",\n//                defaultValue: false, // iPhone users can uncomment these lines!\n                required: true\n            )\n\n            input (\n                name: \"configAutoResetTamperDelay\",\n                title: \"Auto-Reset Tamper Alarm:\\n\" +\n                \"Automatically reset tamper alarms after this time delay.\\n\" +\n                \"Values: 0 = Auto-reset Disabled\\n\" +\n                \"1-86400 = Delay (s)\\n\" +\n                \"Default Value: 30s\",\n                type: \"number\",\n                ,\n//                defaultValue: \"30\", // iPhone users can uncomment these lines!\n                required: false\n            )\n\n        }\n\n        section { // WAKE UP INTERVAL:\n            input (\n                name: \"configWakeUpInterval\",\n                title: \"WAKE UP INTERVAL:\\n\" +\n                \"The device will wake up after each defined time interval to sync configuration parameters, \" +\n                \"associations and settings.\\n\" +\n                \"Values: 5-86399 = Interval (s)\\n\" +\n                \"Default Value: 4000 (every 66 minutes)\",\n                type: \"number\",\n                ,\n//                defaultValue: \"4000\", // iPhone users can uncomment these lines!\n                required: false\n            )\n        }\n\n        generatePrefsParams()\n\n        generatePrefsAssocGroups()\n\n    }\n\n}\n\n/**\n *  parse()\n *\n *  Called when messages from the device are received by the hub. The parse method is responsible for interpreting\n *  those messages and returning event definitions (and command responses).\n *\n *  As this is a Z-wave device, zwave.parse() is used to convert the message into a command. The command is then\n *  passed to zwaveEvent(), which is overloaded for each type of command below.\n *\n *  Parameters:\n *   String      description        The raw message from the device.\n **/\ndef parse(description) {\n    logger(\"parse(): Parsing raw message: ${description}\",\"trace\")\n\n    def result = []\n\n    if (description.startsWith(\"Err\")) {\n        logger(\"parse(): Unknown Error. Raw message: ${description}\",\"error\")\n    }\n    else {\n\n        // Run testRun() if there is a test pending:\n        if (state.testPending) {\n            testRun()\n        }\n\n        def cmd = zwave.parse(description, getCommandClassVersions())\n        if (cmd) {\n            result += zwaveEvent(cmd)\n\n            // Attempt sync(), but only if the received message is an unsolicited command:\n            if (\n                (cmd.commandClassId == 0x20 )  // Basic\n                || (cmd.commandClassId == 0x30 )  // Sensor Binary\n                || (cmd.commandClassId == 0x31 )  // Sensor Multilevel\n                || (cmd.commandClassId == 0x60 )  // Multichannel (SensorMultilevelReport arrive in Multichannel)\n                || (cmd.commandClassId == 0x71 )  // Alarm\n                || (cmd.commandClassId == 0x84 & cmd.commandId == 0x07) // WakeUpNotification\n                || (cmd.commandClassId == 0x9C )  // Sensor Alarm\n            ) { sync() }\n\n        } else {\n            logger(\"parse(): Could not parse raw message: ${description}\",\"error\")\n        }\n    }\n\n    // Send wakeUpNoMoreInformation command, but only if there is nothing more to sync:\n    if ( (device.latestValue(\"powerSource\") == \"battery\") & (device.latestValue(\"syncPending\").toInteger() == 0) ) {\n        result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())\n    }\n\n    return result\n}\n\n/*****************************************************************************************************************\n *  Z-wave Event Handlers.\n *****************************************************************************************************************/\n\n/**\n *  zwaveEvent( COMMAND_CLASS_BASIC V1 (0x20) : BASIC_SET )\n *\n *  The Basic Set command is used to set a value in a supporting device.\n *\n *  Note: If this command is received by the hub, the hub will be in Associatin Group 1, and parameter #5 set to 255.\n *   The hub should also receive a corresponding SensorAlarmReport anyway.\n *\n *  Action: Log water event.\n *\n *  cmd attributes:\n *    Short    value\n *      0x00       = Off       = Dry\n *      0x01..0x63 = 0..100%   = Wet\n *      0xFE       = Unknown\n *      0xFF       = On        = Wet\n *\n *  Example: BasicSet(value: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {\n    logger(\"zwaveEvent(): Basic Set received: ${cmd}\",\"trace\")\n\n    def map = [:]\n\n    map.name = \"water\"\n    map.value = cmd.value ? \"wet\" : \"dry\"\n    map.descriptionText = \"${device.displayName} is ${map.value}\"\n\n    return createEvent(map)\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SENSOR_BINARY V1 (0x30) : SENSOR_BINARY_REPORT (0x03) )\n *\n *  The Sensor Binary Report command is used to advertise a sensor value.\n *   THIS COMMAND CLASS IS DEPRECIATED!\n *\n *  Action: Do nothing, as we don't event know which sensor the value is from.\n *\n *  Note: The Fibaro Flood Sensor will not send these unless explicitly requested.\n *\n *  cmd attributes:\n *    Short  sensorValue  Sensor Value.\n *\n *  Example: SensorBinaryReport(sensorValue: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) {\n    logger(\"zwaveEvent(): Sensor Binary Report received: ${cmd}\",\"trace\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SENSOR_MULTILEVEL V2 (0x31) : SENSOR_MULTILEVEL_REPORT (0x05) )\n *\n *  The Multilevel Sensor Report Command is used by a multilevel sensor to advertise a sensor reading.\n *\n *  Action: Raise appropriate type of event (and disp event) and log an info message.\n *\n *  Note: SmartThings does not yet have capabilities corresponding to all possible sensor types, therefore\n *  some of the event types raised below are non-standard.\n *\n *  cmd attributes:\n *    Short         precision           Indicates the number of decimals.\n *                                      E.g. The decimal value 1025 with precision 2 is therefore equal to 10.25.\n *    Short         scale               Indicates what unit the sensor uses.\n *    BigDecimal    scaledSensorValue   Sensor value as a double.\n *    Short         sensorType          Sensor Type (8 bits).\n *    List<Short>   sensorValue         Sensor value as an array of bytes.\n *    Short         size                Indicates the number of bytes used for the sensor value.\n *\n *  Example: SensorMultilevelReport(precision: 2, scale: 0, scaledSensorValue: 20.67, sensorType: 1, sensorValue: [0, 0, 8, 19], size: 4)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) {\n    logger(\"zwaveEvent(): SensorMultilevelReport received: ${cmd}\",\"trace\")\n\n    def result = []\n    def map = [ displayed: true, value: cmd.scaledSensorValue.toString() ]\n    def dispMap = [ displayed: false ]\n\n    // Sensor Types up to V4 only, there are further sensor types up to V10 defined.\n    switch (cmd.sensorType) {\n        case 1:  // Air Temperature (V1)\n            map.name = \"temperature\"\n            map.unit = (cmd.scale == 1) ? \"F\" : \"C\"\n            break\n\n        case 2:  // General Purpose (V1)\n            map.name = \"value\"\n            map.unit = (cmd.scale == 1) ? \"\" : \"%\"\n            break\n\n        case 3:  // Luninance (V1)\n            map.name = \"illuminance\"\n            map.unit = (cmd.scale == 1) ? \"lux\" : \"%\"\n            break\n\n        case 4:  // Power (V2)\n            map.name = \"power\"\n            map.unit = (cmd.scale == 1) ? \"Btu/h\" : \"W\"\n            dispMap.name = \"dispPower\"\n            dispMap.value = String.format(\"%.1f\",cmd.scaledSensorValue as BigDecimal) + \" ${map.unit}\"\n            break\n\n        case 5:  // Humidity (V2)\n            map.name = \"humidity\"\n            map.unit = (cmd.scale == 1) ? \"g/m^3\" : \"%\"\n            break\n\n        case 6:  // Velocity (V2)\n            map.name = \"velocity\"\n            map.unit = (cmd.scale == 1) ? \"mph\" : \"m/s\"\n            break\n\n        case 7:  // Direction (V2)\n            map.name = \"direction\"\n            map.unit = \"\"\n            break\n\n        case 8:  // Atmospheric Pressure (V2)\n        case 9:  // Barometric Pressure (V2)\n            map.name = \"pressure\"\n            map.unit = (cmd.scale == 1) ? \"inHg\" : \"kPa\"\n            break\n\n        case 0xA:  // Solar Radiation (V2)\n            map.name = \"radiation\"\n            map.unit = \"W/m^3\"\n            break\n\n        case 0xB:  // Dew Point (V2)\n            map.name = \"dewPoint\"\n            map.unit = (cmd.scale == 1) ? \"F\" : \"C\"\n            break\n\n        case 0xC:  // Rain Rate (V2)\n            map.name = \"rainRate\"\n            map.unit = (cmd.scale == 1) ? \"in/h\" : \"mm/h\"\n            break\n\n        case 0xD:  // Tide Level (V2)\n            map.name = \"tideLevel\"\n            map.unit = (cmd.scale == 1) ? \"ft\" : \"m\"\n            break\n\n        case 0xE:  // Weight (V3)\n            map.name = \"weight\"\n            map.unit = (cmd.scale == 1) ? \"lbs\" : \"kg\"\n            break\n\n        case 0xF:  // Voltage (V3)\n            map.name = \"voltage\"\n            map.unit = (cmd.scale == 1) ? \"mV\" : \"V\"\n            dispMap.name = \"dispVoltage\"\n            dispMap.value = String.format(\"%.1f\",cmd.scaledSensorValue as BigDecimal) + \" ${map.unit}\"\n            break\n\n        case 0x10:  // Current (V3)\n            map.name = \"current\"\n            map.unit = (cmd.scale == 1) ? \"mA\" : \"A\"\n            dispMap.name = \"dispCurrent\"\n            dispMap.value = String.format(\"%.1f\",cmd.scaledSensorValue as BigDecimal) + \" ${map.unit}\"\n            break\n\n        case 0x11:  // Carbon Dioxide Level (V3)\n            map.name = \"carbonDioxide\"\n            map.unit = \"ppm\"\n            break\n\n        case 0x12:  // Air Flow (V3)\n            map.name = \"fluidFlow\"\n            map.unit = (cmd.scale == 1) ? \"cfm\" : \"m^3/h\"\n            break\n\n        case 0x13:  // Tank Capacity (V3)\n            map.name = \"fluidVolume\"\n            map.unit = (cmd.scale == 0) ? \"ltr\" : (cmd.scale == 1) ? \"m^3\" : \"gal\"\n            break\n\n        case 0x14:  // Distance (V3)\n            map.name = \"distance\"\n            map.unit = (cmd.scale == 0) ? \"m\" : (cmd.scale == 1) ? \"cm\" : \"ft\"\n            break\n\n        default:\n            logger(\"zwaveEvent(): SensorMultilevelReport with unhandled sensorType: ${cmd}\",\"warn\")\n            map.name = \"unknown\"\n            map.unit = \"unknown\"\n            break\n    }\n\n    logger(\"New sensor reading: Name: ${map.name}, Value: ${map.value}, Unit: ${map.unit}\",\"info\")\n\n    result << createEvent(map)\n    if (dispMap.name) { result << createEvent(dispMap) }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MULTICHANNEL V4 (0x60) : MULTI_CHANNEL_CMD_ENCAP (0x0D))\n *\n *  The Multi Channel Command Encapsulation command is used to encapsulate commands. Any command supported by\n *  a Multi Channel End Point may be encapsulated using this command.\n *\n *  Action: Extract the encapsulated command and pass to the appropriate zwaveEvent() handler.\n *\n *  cmd attributes:\n *    Boolean      bitAddress           Set to true if multicast addressing is used.\n *    Short        command              Command identifier of the embedded command.\n *    Short        commandClass         Command Class identifier of the embedded command.\n *    Short        destinationEndPoint  Destination End Point.\n *    List<Short>  parameter            Carries the parameter(s) of the embedded command.\n *    Short        sourceEndPoint       Source End Point.\n *\n *  Example: MultiChannelCmdEncap(bitAddress: false, command: 1, commandClass: 32, destinationEndPoint: 0,\n *            parameter: [0], sourceEndPoint: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {\n    logger(\"zwaveEvent(): Multi Channel Command Encapsulation command received: ${cmd}\",\"trace\")\n\n    def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())\n    if (!encapsulatedCommand) {\n        logger(\"zwaveEvent(): Could not extract command from ${cmd}\",\"error\")\n    } else {\n        return zwaveEvent(encapsulatedCommand)\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_CONFIGURATION V1 (0x70) : CONFIGURATION_REPORT (0x06) )\n *\n *  The Configuration Report Command is used to advertise the actual value of the advertised parameter.\n *\n *  Action: Store the value in the parameter cache, update syncPending, and log an info message.\n *\n *  Note: The Fibaro Flood Sensor documentation treats some parameter values as SIGNED and others as UNSIGNED!\n *   configurationValues are converted accordingly, using the isSigned attribute from getParamMd().\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  cmd attributes:\n *    List<Short>  configurationValue        Value of parameter (byte array).\n *    Short        parameterNumber           Parameter ID.\n *    Integer      scaledConfigurationValue  Value of parameter (as signed int).\n *    Short        size                      Size of parameter's value (bytes).\n *\n *  Example: ConfigurationReport(configurationValue: [0], parameterNumber: 14, reserved11: 0,\n *            scaledConfigurationValue: 0, size: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {\n    logger(\"zwaveEvent(): Configuration Report received: ${cmd}\",\"trace\")\n\n    def paramMd = getParamsMd().find( { it.id == cmd.parameterNumber })\n    // Some values are treated as unsigned and some as signed, so we convert accordingly:\n    def paramValue = (paramMd?.isSigned) ? cmd.scaledConfigurationValue : byteArrayToUInt(cmd.configurationValue)\n    def signInfo = (paramMd?.isSigned) ? \"SIGNED\" : \"UNSIGNED\"\n\n    state.\"paramCache${cmd.parameterNumber}\" = paramValue\n    logger(\"Parameter #${cmd.parameterNumber} [${paramMd?.name}] has value: ${paramValue} [${signInfo}]\",\"info\")\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_NOTIFICATION V3 (0x71) : NOTIFICATION_REPORT (0x05) )\n *\n *  The Notification Report Command is used to advertise notification information.\n *\n *  Action: Raise appropriate type of event (e.g. fault, tamper, water) and log an info or warn message.\n *\n *  Note: SmartThings does not yet have official capabilities definited for many types of notification. E.g. this\n *  handler raises 'fault' events, which is not part of any standard capability.\n *\n *  cmd attributes:\n *    Short        event                  Event Type (see code below).\n *    List<Short>  eventParameter         Event Parameter(s) (depends on Event type).\n *    Short        eventParametersLength  Length of eventParameter.\n *    Short        notificationStatus     The notification reporting status of the device (depends on push or pull model).\n *    Short        notificationType       Notification Type (see code below).\n *    Boolean      sequence\n *    Short        v1AlarmLevel           Legacy Alarm Level from Alarm CC V1.\n *    Short        v1AlarmType            Legacy Alarm Type from Alarm CC V1.\n *    Short        zensorNetSourceNodeId  Source node ID\n *\n *  Example: NotificationReport(event: 8, eventParameter: [], eventParametersLength: 0, notificationStatus: 255,\n *    notificationType: 8, reserved61: 0, sequence: false, v1AlarmLevel: 0, v1AlarmType: 0, zensorNetSourceNodeId: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {\n    logger(\"zwaveEvent(): Notification Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    switch (cmd.notificationType) {\n        //case 1:  // Smoke Alarm: // Not Implemented yet. Should raise smoke/carbonMonoxide/consumableStatus events etc...\n        //case 2:  // CO Alarm:\n        //case 3:  // CO2 Alarm:\n\n        case 4:  // Heat Alarm:\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                    // Do not send a fault clear event automatically.\n                    logger(\"Heat Alarm Cleared\",\"info\")\n                    break\n\n                case 1:  // Overheat detected:\n                case 2:  // Overheat detected, Unknown Location:\n                    result << createEvent(name: \"fault\", value: \"overheat\", descriptionText: \"Overheat detected!\", displayed: true)\n                    logger(\"Overheat detected!\",\"warn\")\n                    break\n\n                case 3:  // Rapid Temperature Rise:\n                case 4:  // Rapid Temperature Rise, Unknown Location:\n                    result << createEvent(name: \"fault\", value: \"temperature\", descriptionText: \"Rapid temperature rise detected!\", displayed: true)\n                    logger(\"Rapid temperature rise detected!\",\"warn\")\n                    break\n\n                case 5:  // Underheat detected:\n                case 6:  // Underheat detected, Unknown Location:\n                    result << createEvent(name: \"fault\", value: \"underheat\", descriptionText: \"Underheat detected!\", displayed: true)\n                    logger(\"Underheat detected!\",\"warn\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        //case 5:  // Water Alarm: // Not Implemented yet. Should raise water/consumableStatus events etc...\n\n        case 8:  // Power Management:\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                    // Do not send a fault clear event automatically.\n                    logger(\"Previous Events cleared\",\"info\")\n                    break\n\n                //case 1:  // Mains Connected:\n                //case 2:  // AC Mains Disconnected:\n                //case 3:  // AC Mains Re-connected:\n\n                case 4:  // Surge:\n                    result << createEvent(name: \"fault\", value: \"surge\", descriptionText: \"Power surge detected!\", displayed: true)\n                    logger(\"Power surge detected!\",\"warn\")\n                    break\n\n                case 5:  // Voltage Drop:\n                    result << createEvent(name: \"fault\", value: \"voltage\", descriptionText: \"Voltage drop detected!\", displayed: true)\n                    logger(\"Voltage drop detected!\",\"warn\")\n                    break\n\n                case 6:  // Over-current:\n                    result << createEvent(name: \"fault\", value: \"current\", descriptionText: \"Over-current detected!\", displayed: true)\n                    logger(\"Over-current detected!\",\"warn\")\n                    break\n\n                 case 7:  // Over-Voltage:\n                    result << createEvent(name: \"fault\", value: \"voltage\", descriptionText: \"Over-voltage detected!\", displayed: true)\n                    logger(\"Over-voltage detected!\",\"warn\")\n                    break\n\n                 case 8:  // Overload:\n                    result << createEvent(name: \"fault\", value: \"load\", descriptionText: \"Overload detected!\", displayed: true)\n                    logger(\"Overload detected!\",\"warn\")\n                    break\n\n                 case 9:  // Load Error:\n                    result << createEvent(name: \"fault\", value: \"load\", descriptionText: \"Load Error detected!\", displayed: true)\n                    logger(\"Load Error detected!\",\"warn\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        case 9:  // system:\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                    // Do not send a fault clear event automatically.\n                    logger(\"Previous Events cleared\",\"info\")\n                    break\n\n                case 1:  // Harware Failure:\n                case 3:  // Harware Failure (with manufacturer proprietary failure code):\n                    result << createEvent(name: \"fault\", value: \"hardware\", descriptionText: \"Hardware failure detected!\", displayed: true)\n                    logger(\"Hardware failure detected!\",\"warn\")\n                    break\n\n                case 2:  // Software Failure:\n                case 4:  // Software Failure (with manufacturer proprietary failure code):\n                    result << createEvent(name: \"fault\", value: \"firmware\", descriptionText: \"Firmware failure detected!\", displayed: true)\n                    logger(\"Firmware failure detected!\",\"warn\")\n                    break\n\n                case 6:  // Tampering:\n                    result << createEvent(name: \"tamper\", value: \"detected\", descriptionText: \"Tampering: Product covering removed!\", displayed: true)\n                    logger(\"Tampering: Product covering removed!\",\"warn\")\n                    if (state.autoResetTamperDelay > 0) runIn(state.autoResetTamperDelay, \"resetTamper\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        default:\n            logger(\"zwaveEvent(): Notification Report recieved with unhandled notificationType: ${cmd}\",\"warn\")\n            break\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MANUFACTURER_SPECIFIC V2 (0x72) : MANUFACTURER_SPECIFIC_REPORT (0x05) )\n *\n *  Manufacturer-Specific Reports are used to advertise manufacturer-specific information, such as product number\n *  and serial number.\n *\n *  Action: Publish values as device 'data'. Log a warn message if manufacturerId and/or productId do not\n *  correspond to Fibaro Flood Sensor V1.\n *\n *  Example: ManufacturerSpecificReport(manufacturerId: 271, manufacturerName: Fibargroup, productId: 4097,\n *   productTypeId: 2816)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {\n    logger(\"zwaveEvent(): Manufacturer-Specific Report received: ${cmd}\",\"trace\")\n\n    // Display as hex strings:\n    def manufacturerIdDisp = String.format(\"%04X\",cmd.manufacturerId)\n    def productIdDisp = String.format(\"%04X\",cmd.productId)\n    def productTypeIdDisp = String.format(\"%04X\",cmd.productTypeId)\n\n    logger(\"Manufacturer-Specific Report: Manufacturer ID: ${manufacturerIdDisp}, Manufacturer Name: ${cmd.manufacturerName}\" +\n    \", Product Type ID: ${productTypeIdDisp}, Product ID: ${productIdDisp}\",\"info\")\n\n    if ( 271 != cmd.manufacturerId) logger(\"Device Manufacturer is not Fibaro. Using this device handler with a different device may damage your device!\",\"warn\")\n    if ( 4097 != cmd.productId) logger(\"Product ID does not match Fibaro Flood Sensor. Using this device handler with a different device may damage you device!\",\"warn\")\n\n    updateDataValue(\"manufacturerName\",cmd.manufacturerName)\n    updateDataValue(\"manufacturerId\",manufacturerIdDisp)\n    updateDataValue(\"productId\",productIdDisp)\n    updateDataValue(\"productTypeId\",productTypeIdDisp)\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_FIRMWARE_UPDATE_MD V2 (0x7A) : FIRMWARE_MD_REPORT (0x02) )\n *\n *  The Firmware Meta Data Report Command is used to advertise the status of the current firmware in the device.\n *\n *  Action: Publish values as device 'data' and log an info message. No check is performed.\n *\n *  cmd attributes:\n *    Integer  checksum        Checksum of the firmware image.\n *    Integer  firmwareId      Firware ID (this is not the firmware version).\n *    Integer  manufacturerId  Manufacturer ID.\n *\n *  Example: FirmwareMdReport(checksum: 50874, firmwareId: 274, manufacturerId: 271)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd) {\n    logger(\"zwaveEvent(): Firmware Metadata Report received: ${cmd}\",\"trace\")\n\n    // Display as hex strings:\n    def firmwareIdDisp = String.format(\"%04X\",cmd.firmwareId)\n    def checksumDisp = String.format(\"%04X\",cmd.checksum)\n\n    logger(\"Firmware Metadata Report: Firmware ID: ${firmwareIdDisp}, Checksum: ${checksumDisp}\",\"info\")\n\n    updateDataValue(\"firmwareId\",\"${firmwareIdDisp}\")\n    updateDataValue(\"firmwareChecksum\",\"${checksumDisp}\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_BATTERY V1 (0x80) : BATTERY_REPORT (0x03) )\n *\n *  The Battery Report command is used to report the battery level of a battery operated device.\n *\n *  Action: Raise battery event and log an info message.\n *\n *  cmd attributes:\n *    Integer  batteryLevel  Battery level (%).\n *\n *  Example: BatteryReport(batteryLevel: 52)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {\n    logger(\"zwaveEvent(): Battery Report received: ${cmd}\",\"trace\")\n    logger(\"Battery Level: ${cmd.batteryLevel}%\",\"info\")\n\n    def result = []\n    result << createEvent(name: \"powerSource\", value: \"battery\", descriptionText: \"Device is using battery.\")\n    result << createEvent(name: \"battery\", value: cmd.batteryLevel, unit: \"%\", displayed: true)\n    result << createEvent(name: \"batteryStatus\", value: \"Battery: ${cmd.batteryLevel}%\", displayed: false)\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_WAKE_UP V1 (0x84) : WAKE_UP_INTERVAL_REPORT (0x06) )\n *\n *  The Wake Up Interval Report command is used to report the wake up interval of a device and the NodeID of the\n *  device receiving the Wake Up Notification Command.\n *\n *  Action: cache value, update syncPending, and log info message.\n *\n *  cmd attributes:\n *    nodeid\n *    seconds\n *\n *  Example: WakeUpIntervalReport(nodeid: 1, seconds: 300)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpIntervalReport cmd) {\n    logger(\"zwaveEvent(): Wakeup Interval Report received: ${cmd}\",\"trace\")\n\n    state.wakeUpIntervalCache = cmd.seconds.toInteger()\n    logger(\"Wake Up Interval is ${cmd.seconds} seconds.\",\"info\")\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_WAKE_UP V1 (0x84) : WAKE_UP_NOTIFICATION (0x07) )\n *\n *  The Wake Up Notificaiton command allows a battery-powered device to notify another device that it is awake and\n *  ready to receive any queued commands.\n *\n *  Action: Request BatteryReport, FirmwareMdReport, ManufacturerSpecificReport, and VersionReport.\n *\n *  cmd attributes:\n *    None\n *\n *  Example: WakeUpNotification()\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {\n    logger(\"zwaveEvent(): Wakeup Notification received: ${cmd}\",\"trace\")\n\n    logger(\"Device Woke Up\",\"info\")\n\n    def result = []\n\n    result << response(zwave.batteryV1.batteryGet())\n    result << response(zwave.firmwareUpdateMdV2.firmwareMdGet())\n    result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet())\n    result << response(zwave.versionV1.versionGet())\n\n    // Send wakeUpNoMoreInformation command, but only if there is nothing more to sync:\n    if (device.latestValue(\"syncPending\").toInteger() == 0) result << response(zwave.wakeUpV1.wakeUpNoMoreInformation())\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_ASSOCIATION V2 (0x85) : ASSOCIATION_REPORT (0x03) )\n *\n *  The Association Report command is used to advertise the current destination nodes of a given association group.\n *\n *  Action: Cache value and log info message only.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  Example: AssociationReport(groupingIdentifier: 4, maxNodesSupported: 5, nodeId: [1], reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {\n    logger(\"zwaveEvent(): Association Report received: ${cmd}\",\"trace\")\n\n    state.\"assocGroupCache${cmd.groupingIdentifier}\" = cmd.nodeId\n\n    // Display to user in hex format (same as IDE):\n    def hexArray  = []\n    cmd.nodeId.sort().each { hexArray.add(String.format(\"%02X\", it)) };\n    logger(\"Association Group ${cmd.groupingIdentifier} contains nodes: ${hexArray} (hexadecimal format)\",\"info\")\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_VERSION V1 (0x86) : VERSION_REPORT (0x12) )\n *\n *  The Version Report Command is used to advertise the library type, protocol version, and application version.\n\n *  Action: Publish values as device 'data' and log an info message. No check is performed.\n *\n *  Note: Device actually supports V2, but SmartThings only supports V1.\n *\n *  cmd attributes:\n *    Short  applicationSubVersion\n *    Short  applicationVersion\n *    Short  zWaveLibraryType\n *    Short  zWaveProtocolSubVersion\n *    Short  zWaveProtocolVersion\n *\n *  Example: VersionReport(applicationSubVersion: 4, applicationVersion: 3, zWaveLibraryType: 3,\n *   zWaveProtocolSubVersion: 5, zWaveProtocolVersion: 4)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {\n    logger(\"zwaveEvent(): Version Report received: ${cmd}\",\"trace\")\n\n    def zWaveLibraryTypeDisp  = String.format(\"%02X\",cmd.zWaveLibraryType)\n    def zWaveLibraryTypeDesc  = \"\"\n    switch(cmd.zWaveLibraryType) {\n        case 1:\n            zWaveLibraryTypeDesc = \"Static Controller\"\n            break\n\n        case 2:\n            zWaveLibraryTypeDesc = \"Controller\"\n            break\n\n        case 3:\n            zWaveLibraryTypeDesc = \"Enhanced Slave\"\n            break\n\n        case 4:\n            zWaveLibraryTypeDesc = \"Slave\"\n            break\n\n        case 5:\n            zWaveLibraryTypeDesc = \"Installer\"\n            break\n\n        case 6:\n            zWaveLibraryTypeDesc = \"Routing Slave\"\n            break\n\n        case 7:\n            zWaveLibraryTypeDesc = \"Bridge Controller\"\n            break\n\n        case 8:\n            zWaveLibraryTypeDesc = \"Device Under Test (DUT)\"\n            break\n\n        case 0x0A:\n            zWaveLibraryTypeDesc = \"AV Remote\"\n            break\n\n        case 0x0B:\n            zWaveLibraryTypeDesc = \"AV Device\"\n            break\n\n        default:\n            zWaveLibraryTypeDesc = \"N/A\"\n    }\n\n    def applicationVersionDisp = String.format(\"%d.%02d\",cmd.applicationVersion,cmd.applicationSubVersion)\n    def zWaveProtocolVersionDisp = String.format(\"%d.%02d\",cmd.zWaveProtocolVersion,cmd.zWaveProtocolSubVersion)\n\n    logger(\"Version Report: Application Version: ${applicationVersionDisp}, \" +\n           \"Z-Wave Protocol Version: ${zWaveProtocolVersionDisp}, \" +\n           \"Z-Wave Library Type: ${zWaveLibraryTypeDisp} (${zWaveLibraryTypeDesc})\",\"info\")\n\n    updateDataValue(\"applicationVersion\",\"${cmd.applicationVersion}\")\n    updateDataValue(\"applicationSubVersion\",\"${cmd.applicationSubVersion}\")\n    updateDataValue(\"zWaveLibraryType\",\"${zWaveLibraryTypeDisp}\")\n    updateDataValue(\"zWaveProtocolVersion\",\"${cmd.zWaveProtocolVersion}\")\n    updateDataValue(\"zWaveProtocolSubVersion\",\"${cmd.zWaveProtocolSubVersion}\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION V2 (0x8E) : ASSOCIATION_REPORT (0x03) )\n *\n *  The Multi-channel Association Report command is used to advertise the current destinations of a given\n *  association group (nodes and endpoints).\n *\n *  Action: Store the destinations in the assocGroup cache, update syncPending, and log an info message.\n *   Also, if maxNodesSupported is reported as zero for Assoc Group #3, then disable future sync of this group.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  Example: MultiChannelAssociationReport(groupingIdentifier: 2, maxNodesSupported: 8, nodeId: [9,0,1,1,2,3],\n *            reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) {\n    logger(\"zwaveEvent(): Multi-Channel Association Report received: ${cmd}\",\"trace\")\n\n    state.\"assocGroupCache${cmd.groupingIdentifier}\" = cmd.nodeId // Must not sort as order is important.\n\n    def assocGroupName = getAssocGroupsMd().find( { it.id == cmd.groupingIdentifier} ).name\n    // Display to user in hex format (same as IDE):\n    def hexArray  = []\n    cmd.nodeId.each { hexArray.add(String.format(\"%02X\", it)) };\n    logger(\"Association Group #${cmd.groupingIdentifier} [${assocGroupName}] contains destinations: ${hexArray}\",\"info\")\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SENSOR_ALARM V1 (0x9C) : SENSOR_ALARM_REPORT (0x02) )\n *\n *  The Sensor Alarm Report command is used to advertise the alarm state.\n *   THIS COMMAND CLASS IS DEPRECIATED! But still used by the device.\n *\n *  Action: Raies water or tamper event. Log info message.\n *\n *  cmd attributes:\n *    Integer  seconds       Time the alarm has been active.\n *    Short    sensorState   Sensor state.\n *      0x00      = No Alarm\n *      0x01-0x64 = Alarm Severity\n *      0xFF      = Alarm.\n *    Short    sensorType    Sensor Type.\n *    Short    sourceNodeId  Z-Wave node ID of sending device.\n *\n *  Example: SensorAlarmReport(seconds: 0, sensorState: 255, sensorType: 0, sourceNodeId: 7)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) {\n    logger(\"zwaveEvent(): Sensor Alarm Report received: ${cmd}\",\"trace\")\n\n    def map = [:]\n\n    switch (cmd.sensorType) {\n        case 0:  // General Purpose Alarm\n        case 1:  // Smoke Alarm (but used here as tamper)\n            map.name = \"tamper\"\n            map.isStateChange = true\n            map.value = cmd.sensorState ? \"detected\" : \"clear\"\n            map.descriptionText = \"${device.displayName} has been tampered with.\"\n            logger(\"Device has been tampered with!\",\"info\")\n            if (state.autoResetTamperDelay > 0) runIn(state.autoResetTamperDelay, \"resetTamper\")\n            break\n\n        case 5:  // Water Leak Alarm\n            map.name = \"water\"\n            map.isStateChange = true\n            map.value = cmd.sensorState ? \"wet\" : \"dry\"\n            map.descriptionText = \"${device.displayName} is ${map.value}.\"\n            logger(\"Device is ${map.value}!\",\"info\")\n        break\n\n        default:\n            logger(\"zwaveEvent(): SensorAlarmReport with unhandled sensorType: ${cmd}\",\"warn\")\n            map.name = \"unknown\"\n            map.value = cmd.sensorState\n            break\n    }\n\n    return createEvent(map)\n}\n\n/**\n *  zwaveEvent( DEFAULT CATCHALL )\n *\n *  Called for all commands that aren't handled above.\n **/\ndef zwaveEvent(physicalgraph.zwave.Command cmd) {\n    logger(\"zwaveEvent(): No handler for command: ${cmd}\",\"error\")\n}\n\n\n/*****************************************************************************************************************\n *  Capability-related Commands: [None]\n *****************************************************************************************************************/\n\n\n/*****************************************************************************************************************\n *  Custom Commands:\n *****************************************************************************************************************/\n\n/**\n *  resetTamper()\n *\n *  Clear tamper status.\n **/\ndef resetTamper() {\n    logger(\"resetTamper(): Resetting tamper alarm.\",\"info\")\n    sendEvent(name: \"tamper\", value: \"clear\", descriptionText: \"Tamper alarm cleared\", displayed: true)\n}\n\n/*****************************************************************************************************************\n *  SmartThings System Commands:\n *****************************************************************************************************************/\n\n/**\n *  installed()\n *\n *  Runs when the device is first installed.\n *\n *  Action: Set initial values for internal state.\n **/\ndef installed() {\n    log.trace \"installed()\"\n\n    state.installedAt = now()\n    state.loggingLevelIDE     = 5\n    state.loggingLevelDevice  = 2\n\n    // Initial settings:\n    logger(\"Performing initial setup\",\"info\")\n    sendEvent(name: \"tamper\", value: \"clear\", descriptionText: \"Tamper cleared\", displayed: false)\n    sendEvent(name: \"water\", value: \"dry\", displayed: false)\n\n    if (getZwaveInfo()?.zw?.startsWith(\"L\")) {\n        logger(\"Device is in listening mode (powered).\",\"info\")\n        sendEvent(name: \"powerSource\", value: \"dc\", descriptionText: \"Device is connected to DC power supply.\")\n        sendEvent(name: \"batteryStatus\", value: \"DC-power\", displayed: false)\n    }\n    else {\n        logger(\"Device is in sleepy mode (battery).\",\"info\")\n        sendEvent(name: \"powerSource\", value: \"battery\", descriptionText: \"Device is using battery.\")\n        state.wakeUpIntervalTarget = 300\n    }\n\n    state.paramTarget74 = 3 // enable movement and tmp alerts at start to help sync.\n    state.assocGroupTarget3 = [ zwaveHubNodeId ]\n    sync()\n\n    // Request extra info (same as wakeup):\n    def cmds = []\n    cmds << zwave.batteryV1.batteryGet()\n    cmds << zwave.firmwareUpdateMdV2.firmwareMdGet()\n    cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet()\n    cmds << zwave.versionV1.versionGet()\n    sendSequence(cmds, 400)\n\n}\n\n/**\n *  updated()\n *\n *  Runs when the user hits \"Done\" from Settings page.\n *\n *  Action: Process new settings, set targets for wakeup interval, parameters, and association groups (ready for next sync).\n *\n *  Note: Weirdly, update() seems to be called twice. So execution is aborted if there was a previous execution\n *  within two seconds. See: https://community.smartthings.com/t/updated-being-called-twice/62912\n **/\ndef updated() {\n    logger(\"updated()\",\"trace\")\n\n    if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {\n        state.updatedLastRanAt = now()\n\n        // Update internal state:\n        state.loggingLevelIDE       = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3\n        state.loggingLevelDevice    = (settings.configLoggingLevelDevice) ? settings.configLoggingLevelDevice.toInteger(): 2\n        state.syncAll               = (\"true\" == settings.configSyncAll)\n        state.autoResetTamperDelay  = (settings.configAutoResetTamperDelay) ? settings.configAutoResetTamperDelay.toInteger() : 0\n\n        // Update Wake Up Interval target:\n        state.wakeUpIntervalTarget = (settings.configWakeUpInterval) ? settings.configWakeUpInterval.toInteger() : 3600\n\n        // Update Parameter target values:\n        getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n            state.\"paramTarget${it.id}\" = settings.\"configParam${it.id}\"?.toInteger()\n        }\n\n        // Update Assoc Group target values:\n        getAssocGroupsMd().findAll( { it.id != 3} ).each {\n            state.\"assocGroupTarget${it.id}\" = parseAssocGroupInput(settings.\"configAssocGroup${it.id}\", it.maxNodes)\n        }\n        // Assoc Group #3 will contain controller only:\n        state.assocGroupTarget3 = [ zwaveHubNodeId ]\n\n        (device.latestValue(\"powerSource\") == \"dc\") ? sync() : updateSyncPending()\n\n    }\n    else {\n        logger(\"updated(): Ran within last 2 seconds so aborting.\",\"debug\")\n    }\n}\n\n/*****************************************************************************************************************\n *  Private Helper Functions:\n *****************************************************************************************************************/\n\n/**\n *  logger()\n *\n *  Wrapper function for all logging:\n *    Logs messages to the IDE (Live Logging), and also keeps a historical log of critical error and warning\n *    messages by sending events for the device's logMessage attribute.\n *    Configured using configLoggingLevelIDE and configLoggingLevelDevice preferences.\n **/\nprivate logger(msg, level = \"debug\") {\n\n    switch(level) {\n        case \"error\":\n            if (state.loggingLevelIDE >= 1) log.error msg\n            if (state.loggingLevelDevice >= 1) sendEvent(name: \"logMessage\", value: \"ERROR: ${msg}\", displayed: false, isStateChange: true)\n            break\n\n        case \"warn\":\n            if (state.loggingLevelIDE >= 2) log.warn msg\n            if (state.loggingLevelDevice >= 2) sendEvent(name: \"logMessage\", value: \"WARNING: ${msg}\", displayed: false, isStateChange: true)\n            break\n\n        case \"info\":\n            if (state.loggingLevelIDE >= 3) log.info msg\n            break\n\n        case \"debug\":\n            if (state.loggingLevelIDE >= 4) log.debug msg\n            break\n\n        case \"trace\":\n            if (state.loggingLevelIDE >= 5) log.trace msg\n            break\n\n        default:\n            log.debug msg\n            break\n    }\n}\n\n/**\n *  parseAssocGroupInput(string, maxNodes)\n *\n *  Converts a comma-delimited string of destinations (nodes and endpoints) into an array suitable for passing to\n *  multiChannelAssociationSet(). All numbers are interpreted as hexadecimal. Anything that's not a valid node or\n *  endpoint is discarded (warn). If the list has more than maxNodes, the extras are discarded (warn).\n *\n *  Example input strings:\n *    \"9,A1\"      = Nodes: 9 & 161 (no multi-channel endpoints)            => Output: [9, 161]\n *    \"7,8:1,8:2\" = Nodes: 7, Endpoints: Node8:endpoint1 & node8:endpoint2 => Output: [7, 0, 8, 1, 8, 2]\n */\nprivate parseAssocGroupInput(string, maxNodes) {\n    logger(\"parseAssocGroupInput(): Parsing Association Group Nodes: ${string}\",\"trace\")\n\n    // First split into nodes and endpoints. Count valid entries as we go.\n    if (string) {\n        def nodeList = string.split(',')\n        def nodes = []\n        def endpoints = []\n        def count = 0\n\n        nodeList = nodeList.each { node ->\n            node = node.trim()\n            if ( count >= maxNodes) {\n                logger(\"parseAssocGroupInput(): Number of nodes and endpoints is greater than ${maxNodes}! The following node was discarded: ${node}\",\"warn\")\n            }\n            else if (node.matches(\"\\\\p{XDigit}+\")) { // There's only hexadecimal digits = nodeId\n                def nodeId = Integer.parseInt(node,16)  // Parse as hex\n                if ( (nodeId > 0) & (nodeId < 256) ) { // It's a valid nodeId\n                    nodes << nodeId\n                    count++\n                }\n                else {\n                    logger(\"parseAssocGroupInput(): Invalid nodeId: ${node}\",\"warn\")\n                }\n            }\n            else if (node.matches(\"\\\\p{XDigit}+:\\\\p{XDigit}+\")) { // endpoint e.g. \"0A:2\"\n                def endpoint = node.split(\":\")\n                def nodeId = Integer.parseInt(endpoint[0],16) // Parse as hex\n                def endpointId = Integer.parseInt(endpoint[1],16) // Parse as hex\n                if ( (nodeId > 0) & (nodeId < 256) & (endpointId > 0) & (endpointId < 256) ) { // It's a valid endpoint\n                    endpoints.addAll([nodeId,endpointId])\n                    count++\n                }\n                else {\n                    logger(\"parseAssocGroupInput(): Invalid endpoint: ${node}\",\"warn\")\n                }\n            }\n            else {\n                logger(\"parseAssocGroupInput(): Invalid nodeId: ${node}\",\"warn\")\n            }\n        }\n\n        return (endpoints) ? nodes + [0] + endpoints : nodes\n    }\n    else {\n        return []\n    }\n}\n\n/**\n *  sync()\n *\n *  Manages synchronisation of parameters, association groups, and wake up interval with the physical device.\n *  The syncPending attribute advertises remaining number of sync operations.\n *\n *  Does not return a list of commands, it sends them immediately using sendSequence().\n *\n *  Parameters:\n *   forceAll    Force all items to be synced, otherwise only changed items will be synced.\n **/\nprivate sync(forceAll = false) {\n    logger(\"sync(): Syncing configuration with the physical device.\",\"info\")\n\n    def cmds = []\n    def syncPending = 0\n\n    if (forceAll || state.syncAll) { // Clear all cached values.\n        state.wakeUpIntervalCache = null\n        getParamsMd().findAll( {!it.readonly} ).each { state.\"paramCache${it.id}\" = null }\n        getAssocGroupsMd().each { state.\"assocGroupCache${it.id}\" = null }\n        state.syncAll = false\n    }\n\n    if ( (device.latestValue(\"powerSource\") != \"dc\") & (state.wakeUpIntervalTarget != null) & (state.wakeUpIntervalTarget != state.wakeUpIntervalCache)) {\n        cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds: state.wakeUpIntervalTarget, nodeid: zwaveHubNodeId)\n        cmds << zwave.wakeUpV1.wakeUpIntervalGet().format()\n        logger(\"sync(): Syncing Wake Up Interval: New Value: ${state.wakeUpIntervalTarget}\",\"info\")\n        syncPending++\n    }\n\n    getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n        if ( (state.\"paramTarget${it.id}\" != null) & (state.\"paramCache${it.id}\" != state.\"paramTarget${it.id}\") ) {\n            // configurationSet will detect if scaledConfigurationValue is SIGNEd or UNSIGNED and convert accordingly:\n            cmds << zwave.configurationV1.configurationSet(parameterNumber: it.id, size: it.size, scaledConfigurationValue: state.\"paramTarget${it.id}\".toInteger())\n            cmds << zwave.configurationV1.configurationGet(parameterNumber: it.id)\n            logger(\"sync(): Syncing parameter #${it.id} [${it.name}]: New Value: \" + state.\"paramTarget${it.id}\",\"info\")\n            syncPending++\n        }\n    }\n\n    getAssocGroupsMd().each {\n        def cachedNodes = state.\"assocGroupCache${it.id}\"\n        def targetNodes = state.\"assocGroupTarget${it.id}\"\n\n        if ( cachedNodes != targetNodes ) {\n            // Display to user in hex format (same as IDE):\n            def targetNodesHex  = []\n            targetNodes.each { targetNodesHex.add(String.format(\"%02X\", it)) }\n            logger(\"sync(): Syncing Association Group #${it.id}: Destinations: ${targetNodesHex}\",\"info\")\n            if (it.id  == 3) { // Assoc Group #3 does not support multi-channel, must use regular associationV2.\n                cmds << zwave.associationV2.associationSet(groupingIdentifier: it.id, nodeId: []) // Remove All\n                cmds << zwave.associationV2.associationSet(groupingIdentifier: it.id, nodeId:[zwaveHubNodeId])\n                cmds << zwave.associationV2.associationGet(groupingIdentifier: it.id)\n            }\n            else {\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: it.id, nodeId: []) // Remove All\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: it.id, nodeId: targetNodes)\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: it.id)\n            }\n\n            syncPending++\n        }\n    }\n\n    sendEvent(name: \"syncPending\", value: syncPending, displayed: false)\n    sendSequence(cmds,800) // 800ms seems a reasonable balance.\n}\n\n/**\n *  updateSyncPending()\n *\n *  Updates syncPending attribute, which advertises remaining number of sync operations.\n **/\nprivate updateSyncPending() {\n\n    def syncPending = 0\n\n    if ( (device.latestValue(\"powerSource\") != \"dc\") & (state.wakeUpIntervalTarget != null) & (state.wakeUpIntervalTarget != state.wakeUpIntervalCache)) {\n        syncPending++\n    }\n\n    getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n        if ( (state.\"paramTarget${it.id}\" != null) & (state.\"paramCache${it.id}\" != state.\"paramTarget${it.id}\") ) {\n            syncPending++\n        }\n    }\n\n    getAssocGroupsMd().each {\n        def cachedNodes = state.\"assocGroupCache${it.id}\"\n        def targetNodes = state.\"assocGroupTarget${it.id}\"\n\n        if ( cachedNodes != targetNodes ) {\n            syncPending++\n        }\n    }\n\n    logger(\"updateSyncPending(): syncPending: ${syncPending}\", \"debug\")\n    if ((syncPending == 0) & (device.latestValue(\"syncPending\") > 0)) logger(\"Sync Complete.\", \"info\")\n    sendEvent(name: \"syncPending\", value: syncPending, displayed: false)\n}\n\n/**\n *  refreshConfig()\n *\n *  Request configuration reports from the physical device: [ Configuration, Association,\n *  Manufacturer-Specific, Firmware Metadata, Version, etc. ]\n *\n *  Really only needed at installation or when debugging, as sync will request the necessary reports when the\n *  configuration is changed.\n */\nprivate refreshConfig() {\n    logger(\"refreshConfig()\",\"trace\")\n\n    if (getZwaveInfo()?.zw?.startsWith(\"L\")) {\n        logger(\"Device is in listening mode (powered).\",\"info\")\n        sendEvent(name: \"powerSource\", value: \"dc\", descriptionText: \"Device is connected to DC power supply.\")\n    }\n    else {\n        logger(\"Device is in sleepy mode (battery).\",\"info\")\n        sendEvent(name: \"powerSource\", value: \"battery\", descriptionText: \"Device is using battery.\")\n    }\n\n    def cmds = []\n\n    cmds << zwave.wakeUpV1.wakeUpIntervalGet()\n    cmds << zwave.firmwareUpdateMdV2.firmwareMdGet()\n    cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet()\n    cmds << zwave.versionV1.versionGet()\n\n    getParamsMd().each { cmds << zwave.configurationV1.configurationGet(parameterNumber: it.id) }\n    getAssocGroupsMd().findAll( { it.id != 3 } ).each { cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: it.id) }\n    cmds << zwave.associationV2.associationGet(groupingIdentifier:3)\n\n    sendSequence(cmds, 500) // Delay must be at least 1000 to reliabilty get all results processed.\n}\n\n/**\n *  sendSequence()\n *\n *  Send an array of commands using sendHubCommand.\n **/\nprivate sendSequence(commands, delay = 200) {\n    sendHubCommand(commands.collect{ response(it) }, delay)\n}\n\n/**\n *  generatePrefsParams()\n *\n *  Generates preferences (settings) for device parameters.\n **/\nprivate generatePrefsParams() {\n        section {\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"DEVICE PARAMETERS:\",\n                description: \"Device parameters are used to customise the physical device. \" +\n                             \"Refer to the product documentation for a full description of each parameter.\"\n            )\n\n    getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n\n        def lb = (it.description.length() > 0) ? \"\\n\" : \"\"\n\n        switch(it.type) {\n            case \"number\":\n            input (\n                name: \"configParam${it.id}\",\n                title: \"#${it.id}: ${it.name}: \\n\" + it.description + lb +\"Default Value: ${it.defaultValue}\",\n                type: it.type,\n                range: it.range,\n//                defaultValue: it.defaultValue, // iPhone users can uncomment these lines!\n                required: it.required\n            )\n            break\n\n            case \"enum\":\n            input (\n                name: \"configParam${it.id}\",\n                title: \"#${it.id}: ${it.name}: \\n\" + it.description + lb + \"Default Value: ${it.defaultValue}\",\n                type: it.type,\n                options: it.options,\n//                defaultValue: it.defaultValue, // iPhone users can uncomment these lines!\n                required: it.required\n            )\n            break\n        }\n    }\n        } // section\n}\n\n/**\n *  generatePrefsAssocGroups()\n *\n *  Generates preferences (settings) for Association Groups.\n **/\nprivate generatePrefsAssocGroups() {\n        section {\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"ASSOCIATION GROUPS:\",\n                description: \"Association groups enable this device to control other Z-Wave devices directly, \" +\n                             \"without participation of the main controller.\\n\" +\n                             \"Enter a comma-delimited list of destinations (node IDs and/or endpoint IDs) for \" +\n                             \"each association group. All IDs must be in hexadecimal format. E.g.:\\n\" +\n                             \"Node destinations: '11, 0F'\\n\" +\n                             \"Endpoint destinations: '1C:1, 1C:2'\"\n            )\n\n    getAssocGroupsMd().findAll( { it.id != 3} ).each { // Don't show AssocGroup3 (Lifeline).\n            input (\n                name: \"configAssocGroup${it.id}\",\n                title: \"Association Group #${it.id}: ${it.name}: \\n\" + it.description + \" \\n[MAX NODES: ${it.maxNodes}]\",\n                type: \"text\",\n//                defaultValue: \"\", // iPhone users can uncomment these lines!\n                required: false\n            )\n        }\n    }\n}\n\n/**\n *  byteArrayToUInt(byteArray)\n *\n *  Converts a byte array to an UNSIGNED int.\n **/\nprivate byteArrayToUInt(byteArray) {\n    // return java.nio.ByteBuffer.wrap(byteArray as byte[]).getInt()\n    def i = 0\n    byteArray.reverse().eachWithIndex { b, ix -> i += b * (0x100 ** ix) }\n    return i\n}\n\n/**\n *  test()\n *\n *  Called from 'test' tile.\n **/\nprivate test() {\n    logger(\"test()\",\"trace\")\n    state.testPending = true\n\n    // immediate test actions:\n    def cmds = []\n    //cmds << ...\n    if (cmds) sendSequence(cmds,200)\n}\n\n/**\n *  testRun()\n *\n *  Async Testing method. Called when device wakes up and state.testPending = true.\n **/\nprivate testRun() {\n    logger(\"testRun()\",\"trace\")\n\n    def cmds = []\n    //cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1) //sensorType:1\n    //cmds << zwave.wakeUpV2.wakeUpIntervalCapabilitiesGet()\n    //cmds << zwave.batteryV1.batteryGet()\n\n    if (cmds) sendSequence(cmds,500)\n\n    state.testPending = false\n}\n\n/*****************************************************************************************************************\n *  Static Matadata Functions:\n *\n *  These functions encapsulate metadata about the device. Mostly obtained from:\n *   Z-wave Alliance Reference: http://products.z-wavealliance.org/products/1036\n *****************************************************************************************************************/\n\n/**\n *  getCommandClassVersions()\n *\n *  Returns a map of the command class versions supported by the device. Used by parse() and zwaveEvent() to\n *  extract encapsulated commands from MultiChannelCmdEncap, MultiInstanceCmdEncap, SecurityMessageEncapsulation,\n *  and Crc16Encap messages.\n *\n *  Reference: http://products.z-wavealliance.org/products/1036/classes\n **/\nprivate getCommandClassVersions() {\n    return [0x20: 1, // Basic V1\n            0x30: 1, // Sensor Binary V1 (not even v2).\n            0x31: 2, // Sensor Multilevel V?\n            0x60: 3, // Multi Channel V?\n            0x70: 1, // Configuration V1\n            0x71: 1, // Alarm (Notification) V1\n            0x72: 2, // Manufacturer Specific V2\n            0x7A: 2, // Firmware Update MD V2\n            0x80: 1, // Battery V1\n            0x84: 1, // Wake Up V1\n            0x85: 2, // Association V2\n            0x86: 1, // Version V1\n            0x8E: 2, // Multi Channel Association V2\n            0x9C: 1 // Sensor Alarm V1\n           ]\n}\n\n/**\n *  getParamsMd()\n *\n *  Returns device parameters metadata. Used by sync(), updateSyncPending(), and generatePrefsParams().\n *\n *  Note: The Fibaro documentation treats *some* parameter values as SIGNED and others as UNSIGNED,\n *   e.g.: 1-bit parameters with values 0-255 = UNSIGNED.\n *   The treatment of each parameter is identified in getParamMd() by attribute isSigned.\n *   Unsigned parameter values are converted from signed to unsigned when receiving config reports.\n *\n *  Reference: http://manuals.fibaro.com/flood-sensor/\n **/\nprivate getParamsMd() {\n    return [\n        [id:  1, size: 2, type: \"number\", range: \"0..3600\", defaultValue: 0, required: false, readonly: false,\n         isSigned: true,\n         name: \"Alarm Cancellation Delay\",\n         description: \"The time for which the device will retain the flood state after flooding has ceased.\\n\" +\n         \"Values: 0-3600 = Time Delay (s)\"],\n        [id: 2, size: 1, type: \"enum\", defaultValue: \"3\", required: false, readonly: false,\n         isSigned: true,\n         name: \"Acoustic and Visual Alarms\",\n         description : \"Disable/enable LED indicator and acoustic alarm for flooding detection.\",\n         options: [\"0\" : \"0: Acoustic alarm INACTIVE. Visual alarm INACVTIVE\",\n                   \"1\" : \"1: Acoustic alarm INACTIVE. Visual alarm ACTIVE\",\n                   \"2\" : \"2: Acoustic alarm ACTIVE. Visual alarm INACTIVE\",\n                   \"3\" : \"3: Acoustic alarm ACTIVE. Visual alarm ACTIVE\"] ],\n        [id: 5, size: 1, type: \"enum\", defaultValue: \"255\", required: false, readonly: false,\n         isSigned: false,\n         name: \"Type of Alarm sent to Association Group 1\",\n         description : \"\",\n         options: [\"0\" : \"0: ALARM WATER command\",\n                   \"255\" : \"255: BASIC_SET command\"] ],\n        [id: 7, size: 1, type: \"number\", range: \"1..255\", defaultValue : 255, required: false, readonly: false,\n         isSigned: false,\n         name: \"Level sent to Association Group 1\",\n         description : \"Determines the level sent (BASIC_SET) to Association Group 1 on alarm.\\n\" +\n         \"Values: 1-99 = Level\\n255 = Last memorised state\"],\n        [id: 9, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         isSigned: true,\n         name: \"Alarm Cancelling\",\n         description : \"\",\n         options: [\"0\" : \"0: Alarm cancellation INACTIVE\",\n                   \"1\" : \"1: Alarm cancellation ACTIVE\"] ],\n        [id: 10, size: 2, type: \"number\", range: \"1..65535\", defaultValue : 300, required: false, readonly: false,\n         isSigned: false,\n         name: \"Temperature Measurement Interval\",\n         description : \"Time between consecutive temperature measurements. New temperature value is reported to \" +\n         \"the main controller only if it differs from the previously measured by hysteresis (parameter #12).\\n\" +\n         \"Values: 1-65535 = Time (s)\"],\n        [id: 12, size: 2, type: \"number\", range: \"1..1000\", defaultValue : 50, required: false, readonly: false,\n         isSigned: true,\n         name: \"Temperature Measurement Hysteresis\",\n         description : \"Determines the minimum temperature change resulting in a temperature report being \" +\n         \"sent to the main controller.\\n\" +\n         \"Values: 1-1000 = Temp change (in 0.01C steps)\"],\n        [id: 13, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         isSigned: true,\n         name: \"Alarm Broadcasts\",\n         description : \"Determines if flood and tamper alarms are broadcast to all devices.\",\n         options: [\"0\" : \"0: Flood alarm broadcast INACTIVE. Tamper alarm broadcast INACTIVE\",\n                   \"1\" : \"1: Flood alarm broadcast ACTIVE. Tamper alarm broadcast INACTIVE\",\n                   \"2\" : \"2: Flood alarm broadcast INACTIVE. Tamper alarm broadcast ACTIVE\",\n                   \"3\" : \"3: Flood alarm broadcast ACTIVE. Tamper alarm broadcast ACTIVE\"] ],\n        [id: 50, size: 2, type: \"number\", range: \"-10000..10000\", defaultValue : 1500, required: false, readonly: false,\n         isSigned: true,\n         name: \"Low Temperature Alarm Threshold\",\n         description : \"Temperature below which LED indicator blinks (with a colour determined by Parameter #61).\\n\" +\n         \"Values: -10000-10000 = Temp (-100C to +100C in 0.01C steps)\"],\n        [id: 51, size: 2, type: \"number\", range: \"-10000..10000\", defaultValue : 3500, required: false, readonly: false,\n         isSigned: true,\n         name: \"High Temperature Alarm Threshold\",\n         description : \"Temperature above which LED indicator blinks (with a colour determined by Parameter #62).\\n\" +\n         \"Values: -10000-10000 = Temp (-100C to +100C in 0.01C steps)\"],\n        [id: 61, size: 4, type: \"number\", range: \"0..16777215\", defaultValue : 255, required: false, readonly: false,\n         isSigned: false,\n         name: \"Low Temperature Alarm indicator Colour\",\n         description : \"Indicated colour = 65536 * RED value + 256 * GREEN value + BLUE value.\\n\" +\n         \"Values: 0-16777215\"],\n        [id: 62, size: 4, type: \"number\", range: \"0..16777215\", defaultValue : 16711680, required: false, readonly: false,\n         isSigned: false,\n         name: \"High Temperature Alarm indicator Colour\",\n         description : \"Indicated colour = 65536 * RED value + 256 * GREEN value + BLUE value.\\n\" +\n         \"Values: 0-16777215\"],\n        [id: 63, size: 1, type: \"enum\", defaultValue: \"2\", required: false, readonly: false,\n         isSigned: true,\n         name: \"LED Indicator Operation\",\n         description : \"LED Indicator can be turned off to save battery.\",\n         options: [\"0\" : \"0: OFF\",\n                   \"1\" : \"1: BLINK (every temperature measurement)\",\n                   \"2\" : \"2: CONTINUOUS (constant power only)\"] ],\n        [id: 73, size: 2, type: \"number\", range: \"-10000..10000\", defaultValue : 0, required: false, readonly: false,\n         isSigned: true,\n         name: \"Temperature Measurement Compensation\",\n         description : \"Temperature value to be added to or deducted to compensate for the difference between air \" +\n         \"temperature and temperature at the floor level.\\n\" +\n         \"Values: -10000-10000 = Temp (-100C to +100C in 0.01C steps)\"],\n        [id: 74, size: 1, type: \"enum\", defaultValue: \"2\", required: false, readonly: false,\n         isSigned: true,\n         name: \"Alarm Frame Sent to Association Group #2\",\n         description : \"Turn on alarms resulting from movement and/or the TMP button released.\",\n         options: [\"0\" : \"0: TMP Button INACTIVE. Movement INACTIVE\",\n                   \"1\" : \"1: TMP Button ACTIVE. Movement INACTIVE\",\n                   \"2\" : \"2: TMP Button INACTIVE. Movement ACTIVE\",\n                   \"3\" : \"3: TMP Button ACTIVE. Movement ACTIVE\"] ],\n        [id: 75, size: 2, type: \"number\", range: \"0..65535\", defaultValue : 0, required: false, readonly: false,\n         isSigned: false,\n         name: \"Visual and Audible Alarms Duration\",\n         description : \"Time period after which the LED and audible alarm the will become quiet. ignored when parameter #2 is 0.\\n\" +\n         \"Values: 0 = Active indefinitely\\n\" +\n         \"1-65535 = Time (s)\"],\n        [id: 76, size: 2, type: \"number\", range: \"0..65535\", defaultValue : 0, required: false, readonly: false,\n         isSigned: false,\n         name: \"Alarm Retransmission Time\",\n         description : \"Time period after which an alarm frame will be retransmitted.\\n\" +\n         \"Values: 0 = No retransmission\\n\" +\n         \"1-65535 = Time (s)\"],\n        [id: 77, size: 1, type: \"enum\", defaultValue: \"0\", required: false, readonly: false,\n         isSigned: true,\n         name: \"Flood Sensor Functionality\",\n         description : \"Allows for turning off the internal flood sensor. Tamper and temperature sensor will remain active.\",\n         options: [\"0\" : \"0: Flood sensor ACTIVE\",\n                   \"1\" : \"1: Flood sensor INACTIVE\"] ]\n    ]\n}\n\n/**\n *  getAssocGroupsMd()\n *\n *  Returns association groups metadata. Used by sync(), updateSyncPending(), and generatePrefsAssocGroups().\n *\n *  Reference: http://manuals.fibaro.com/flood-sensor/\n **/\nprivate getAssocGroupsMd() {\n    return [\n        [id:  1, maxNodes: 5, name: \"Device Status\", // Water state?\n         description : \"Reports device state, sending BASIC SET or ALARM commands.\"],\n        [id:  2, maxNodes: 5, name: \"TMP Button and Tilt Sensor\",\n         description : \"Sends ALARM commands to associated devices when TMP button is released or a tilt is triggered (depending on parameter 74).\"],\n        [id:  3, maxNodes: 0, name: \"Device Status\",\n         description : \"Reports device state. Main Z-Wave controller should be added to this group.\"]\n    ]\n}\n"
  },
  {
    "path": "devices/fibaro-rgbw-controller/README.md",
    "content": "# Fibaro RGBW Controller (FGRGBWM-441)\nhttps://github.com/codersaur/SmartThings/tree/master/devices/fibaro-rgbw-controller\n\nCopyright (c) [David Lomas](https://github.com/codersaur)\n\n## Overview\n\nThis SmartThings device handler has been written for the Fibaro RGBW Controller (FGRGBWM-441). It extends the native SmartThings device handler to support editing the device's parameters from the SmartThings GUI, and to support the use of one or more of the controller's channels in IN/OUT mode (i.e. analog sensor inputs).\n\n### Key features:\n* Physical device parameters can be edited from the Smartthings GUI, and verified in the IDE Log.\n* Channels can be mapped to different colours without needing to physically rewire the device.\n* Shortcut tiles for the built-in RGBW programs.\n* Shortcut tiles for named colours.\n* Multiple options for the calculation of aggregate `switch` and `level` attributes (useful when using a combination of inputs and outputs).\n* Configurable thresholds for mapping the level of input channels to their corresponding `switch` (on/off) states.\n* Implements \"Energy Meter\", \"Power Meter\", and \"Polling\" capabilities.\n* For SmartApp developers, the `setColor()` command supports an extended range of colorMap key combinations:\n * red, green, blue, white\n * red, green, blue\n * hue, saturation, level\n * hex\n * name\n* Extensive inline code comments to support community development.\n\n### Screenshots:\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_rgbw.png\" width=\"200\">\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_rgb_plus_input.png\" width=\"200\">\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_power_energy2.png\" width=\"200\">\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_four_inputs.png\" width=\"200\">\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_color_shortcuts.png\" width=\"200\">\n\n## Installation\nTo install the device handler:\n\n1. Follow [these instructions](https://github.com/codersaur/SmartThings#device-handler-installation-procedure) to install the device handler in the SmartThings IDE.\n\n2. From the SmartThings IDE, edit the device handler code to suit your needs. Specifically, the tiles section will need to be customised to suit the channel configuration in use (see the use cases below). [If you have multiple FIbaro RGBW Controllers, each with different channel configurations, then you will need to create multiple copies of the device handler with different names.]\n\n3. From the SmartThings app, edit the device settings to suit the channel configuration in use (see the use cases below) and hit _Done_. [It is possible to verify the device parameters from the Live Logging tab in the SmartThings IDE.]\n\n4. Once the settings have been applied, power-cycle the Fibaro RGBW Controller.\n\n### Example Use Cases\n\n#### Four-channel RGBW LED strip:\n\nBy default, the device handler is configured for use with a four-channel RGBW LED strip, so there is no need to edit the device handler code. The SmartThings GUI should look like the following:\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_rgbw.png\" width=\"200\">\n\n#### Three-channel RGB LED strip, plus a 0-10V analog sensor input:\n\nFor this use case, it is recommended to use Channel #1 as Red, Channel #2 as Green, Channel #3 as Blue, and Channel #4 as the analog input.\n\nIn the device handler code, edit the tiles section to comment out the _White_ channel tiles:\n\n    \"switchWhite\", \"levelWhiteSlider\", \"levelWhiteTile\",\n\nThen uncomment the read-only input channel for Ch4:\n\n    \"switchCh4ReadOnly\", \"ch4Label\", \"levelCh4Tile\",\n\nThe _Built-in Program Shortcut_ tiles can also be commented out as these will not function in this configuration.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/tiles_code_rgb_plus_input.png\" width=\"400\">\n\nIn the SmartThings app, edit the device settings. Configure the channel mappings so that Channel #4 maps to `Input` and Parameter #14 so that Channels #1/2/3 are set to `9. OUT...` and Channel #4 is set to `8. IN - ANALOG 0-10V (SENSOR)`.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/settings_mappings_rgb.png\" width=\"200\">\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/settings_params_rgb_plus_in.png\" width=\"200\">\n\nThe SmartThings GUI should end up looking like the following:\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_rgb_plus_input.png\" width=\"200\">\n\n\n#### Two single-channel output loads, and two 0-10V analog sensor inputs:\n\nIn this example, channels #1 and #2 are used as outputs (e.g. two circuits of white lights), and channels #3 & #4 are used for analog sensor inputs.\n\nIn the device handler code, edit the tiles section to comment out all of the colour channel tile lines:\n\n    // RGBW Channels:\n    //\"switchRed\",\"levelRedSlider\", \"levelRedTile\",\n    //\"switchGreen\",\"levelGreenSlider\", \"levelGreenTile\",\n    //\"switchBlue\",\"levelBlueSlider\", \"levelBlueTile\",\n    //\"switchWhite\", \"levelWhiteSlider\", \"levelWhiteTile\",\n\nUncomment the lines for the Ch1 and Ch2 OUT channels, and the Ch3 and Ch4 input tiles:\n\n    // OUT Channels:\n    \"switchCh1\",\"levelCh1Slider\", \"levelCh1Tile\",\n    \"switchCh2\",\"levelCh2Slider\", \"levelCh2Tile\",\n    ...\n\n    // INPUT Channels (read-only, label replaced slider control):\n    ...\n    \"switchCh3ReadOnly\", \"ch3Label\", \"levelCh3Tile\",\n    \"switchCh4ReadOnly\", \"ch4Label\", \"levelCh4Tile\",\n\nThe _Built-in Program Shortcut_ and _Color Shortcut_ tiles can also be commented out as these will not function in this configuration.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/tiles_code_two_out_two_in.png\" width=\"400\">\n\nIn the device settings, configure the channel mappings so that Channels #3 & #4 map to `Input`, and configure Parameter #14 so that Channels #1 & #2 are set to an `OUT ...` mode, and Channels #3 & #4 are set to `8. IN - ANALOG 0-10V (SENSOR)`\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/settings_params_two_out_two_in.png\" width=\"200\">\n\nThe SmartThings GUI should end up looking like the following:\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_two_out_two_in.png\" width=\"200\">\n\n\n#### Four 0-10V analog sensor inputs:\n\nIn this example, all four channels on the RGBW controller are used as analog sensor inputs. As the output channels cannot be used the tiles configuration can be simplified.\n\nIn the device handler code, edit the tiles section to comment out the RGBW channel tiles:\n\n    // RGBW Channels:\n    //\"switchRed\",\"levelRedSlider\", \"levelRedTile\",\n    //\"switchGreen\",\"levelGreenSlider\", \"levelGreenTile\",\n    //\"switchBlue\",\"levelBlueSlider\", \"levelBlueTile\",\n    //\"switchWhite\", \"levelWhiteSlider\", \"levelWhiteTile\",\n\nUncomment the lines for all input tiles:\n\n    // INPUT Channels (read-only, label replaced slider control):\n    \"switchCh1ReadOnly\", \"ch1Label\", \"levelCh1Tile\",\n    \"switchCh2ReadOnly\", \"ch2Label\", \"levelCh2Tile\",\n    \"switchCh3ReadOnly\", \"ch3Label\", \"levelCh3Tile\",\n    \"switchCh4ReadOnly\", \"ch4Label\", \"levelCh4Tile\",\n\nAdditionally, comment out the _Energy and Power_ tiles, the _Built-in Program Shortcut_ tiles, and the _Color Shortcut Tiles_ sections as none of these will function in this configuration.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/tiles_code_four_inputs.png\" width=\"400\">\n\nIn the device settings, configure the channel mappings so that all channels map to `Input`. It is possible to alter the threshold values too, which control the level at which each input is considered \"ON\".\n\nConfigure Parameter #14 so that all channels are set to `8. IN - ANALOG 0-10V (SENSOR)`\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/settings_mappings_four_inputs.png\" width=\"200\">\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/settings_params_four_inputs.png\" width=\"200\">\n\nThe SmartThings GUI should end up looking like the following:\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_four_inputs.png\" width=\"200\">\n\n\n## Physical Device Notes:\n\nSome general notes relating to the Fibaro RGBW Controller:\n\n* Parameter #14 is used to control the mode of each channel. When editing this parameter keep in mind:\n * If using RGBW modes, all channels must have exactly the same mode.\n * Mixing RGBW channels with IN/OUT channels at the same time will cause weird behaviour, for example the IN channels may report incorrect levels (as the INPUT is treated as a switch input for the RGBW channels).\n  * If you want to use one or more channels as analog inputs, then the remaining channels must be set to OUT mode.\n  * If using IN/OUT channel modes, the OUT channels can still be mapped to colours, but the built-in \"RGBW programs\" will have no effect.\n  * switchColorSet commands do not affect INPUT channels, but you can't use switchColorGet to get the level of an INPUT channel either.\n  * Energy and power reports for individual channels are not available, only the aggregate device as a whole.\n\nThere are two known bugs in firmware 25.25, which this device handler attempts to work around:\n\n* BUG: If the device's parameters are changed, the device may stop responding to many Z-Wave _Get_ commands. **It is therefore recommended to power-cycle the device after changing parameters.**\n* BUG: If a basicSet or switchMultilevelSet command is issued to channel 0 or to an INPUT channel, then the levels of all INPUT channels may be incorrectly reported as zero. Incorrect reports will persist until there is a change to the input voltages that is greater than the 'input change threshold' defined by Paramter #43. To avoid this issue, this device handler does not send basicSet or switchMultilevelSet commands to channels in INPUT mode.\n\n## Version History:\n\n#### 2017-04-17: v0.04\n * installed(): Initialises attribute values in addition to state.\n * updated(): Added check to prevent double execution, and to call installed() if not run.\n  \n#### 2016-11-14: v0.03\n * Association Group Members can be edited from the SmartThings GUI.\n  \n#### 2016-11-13: v0.02\n * Fix to preferences definition to prevent crashes on Android.\n * on(): Restores saved levels of channels, but if all saved levels are zero, then all channels are set to 100%.\n * onChX(): If the saved level is zero, then the channel will be set to 100%.\n * installed(): state variables are pre-populated.\n * configure(): Removes all nodes from association group #5 before re-adding the hub's ID.\n\n#### 2016-11-08: v0.01\n * Added support for channels in IN/OUT modes.\n * Physical device parameters can be changed from the Smartthings GUI, and verified in the IDE Log.\n * Added event handlers for: MeterReport, SwitchColorReport, AssociationReport.\n * reset() resets the accumulated energy usage, not the brightness.\n * Added three options for the calculation of aggregate `switch` and `level` attributes (IN Only / OUT Only / ALL).\n * Added support for channel mappings and thresholds.\n * on()/off(): Only sends commands to OUT channels, to avoid changing levels of INPUTS.\n * setLevel(): Added two modes for setting levels (SIMPLE / SCALE).\n * setColor(): Supports colorMaps with red/green/blue/white, hue/saturation/level, hex, or name keys.\n * updated(): Validates settings for parameter #14 and generates a warning if there's a mixture of RGBW and IN/OUT.\n * getSupportedCommands(): New method to encapsulate a map of the command class versions supported by the device.\n * color attribute is now a map [hue: x, saturation: y, ...], as per SmartThings Capabilities Reference.\n * level attributes and sliders now have a have range of 0-100 percent, instead of 0-99.\n * Added *activeProgram* attribute to support Program tiles.\n * Added *colorName* attribute to support Color Shortcut tiles.\n * Added Test tile and ability to interrogate current device parameters.\n * Added Polling capability, which polls all channels, plus energy and power.\n * configure(): Added workaround for bug in configurationV1.configurationSet().\n\n ## To Do:\n * Add an option to use the White channel in preference to RGB channels when the RGB values equate to white.\n * When sending commands, consider Parameter #42 (reporting). If the device is configured not to send reports, or to only send reports generated by inputs (and not the controller), then a request for the appropriate report must be issued (or call refresh()) a couple of seconds after the command has been issued.\n * Add new device preference \"Update ST UI before sending commands\": This will issue fake events to make the GUI more responsive when using long dimming durations.\n * Consider if `setLevel()` in SCALE mode should really be moved to new `setBrightness()` command. Slider on the multiTile can then be linked to either setLevel or setBrightness via settings.\n\n## References\n Some useful links relevant to the development of this device handler:\n* [Fibaro RGBW Controller Z-Wave certification information](http://products.z-wavealliance.org/products/1054)\n* [Color Control Capability](https://community.smartthings.com/t/capability-color-control-color-attribute-command-ambiguous-incorrectly-documented-and-or-improperly-used-in-dths/58018/23)\n* [Using the Switch Color Command Class](https://community.smartthings.com/t/color-switch-z-wave-command-class/19300)\n* [RGB-RGBW colorMap conversion](http://stackoverflow.com/questions/21117842/converting-an-rgbw-color-to-a-standard-rgb-hsb-rappresentation)\n\n## License\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n"
  },
  {
    "path": "devices/fibaro-rgbw-controller/fibaro-rgbw-controller.groovy",
    "content": "/**\n *  Copyright David Lomas (codersaur)\n *\n *  SmartThings Device Handler for: Fibaro RGBW Controller EU v2.x (FGRGBWM-441)\n *\n *  Version: 0.04 (2017-04-17)\n *\n *  Source: https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-rgbw-controller\n *\n *  Author: David Lomas (codersaur)\n *\n *  Description: This SmartThings device handler is written for the Fibaro RGBW Controller (FGRGBWM-441). It extends\n *  the native SmartThings device handler to support editing the device's parameters from the SmartThings GUI, and to\n *  support the use of one or more of the controller's channels in IN/OUT mode.\n *\n *  For full information, including installation instructions, exmples, and version history, see:\n *   https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-rgbw-controller\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n **/\nmetadata {\n    definition (name: \"Fibaro RGBW Controller\", namespace: \"codersaur\", author: \"David Lomas\") {\n        capability \"Actuator\"\n        capability \"Switch\"\n        capability \"Switch Level\"\n        capability \"Color Control\"\n        capability \"Sensor\"\n        capability \"Energy Meter\"\n        capability \"Power Meter\"\n        capability \"Refresh\"\n        capability \"Polling\"\n\n        // Standard Attributes (for the capabilities above):\n        attribute \"switch\", \"enum\", [\"on\", \"off\"]\n        attribute \"level\", \"number\"\n        attribute \"hue\", \"number\"\n        attribute \"saturation\", \"number\"\n        attribute \"color\", \"string\"\n        attribute \"energy\", \"number\"\n        attribute \"power\", \"number\"\n\n        // Custom Attributes:\n        attribute \"activeProgram\", \"number\" // Required for Program Tiles.\n        attribute \"colorName\", \"string\" // Required for Color Shortcut Tiles.\n        attribute \"lastReset\", \"string\" // Last Time that energy reporting period was reset.\n\n        // Custom Commands:\n        command \"test\"\n        command \"getConfigReport\"\n        command \"reset\"\n\n        // Raw Channel attributes and commands:\n        (1..4).each { n ->\n            attribute \"switchCh${n}\", \"enum\", [\"on\", \"off\"]\n            attribute \"levelCh${n}\", \"number\"\n            command \"onCh$n\"\n            command \"offCh$n\"\n            command \"setLevelCh$n\"\n        }\n\n        // Color Channel attributes and commands:\n        [\"Red\", \"Green\", \"Blue\", \"White\"].each { c ->\n            attribute \"switch${c}\", \"enum\", [\"on\", \"off\"]\n            attribute \"level${c}\", \"number\"\n            command \"on${c}\"\n            command \"off${c}\"\n            command \"setLevel${c}\"\n        }\n\n        // Color shortcut commands:\n        command \"black\"\n        command \"white\"\n        command \"red\"\n        command \"green\"\n        command \"blue\"\n        command \"cyan\"\n        command \"magenta\"\n        command \"orange\"\n        command \"purple\"\n        command \"yellow\"\n        command \"pink\"\n        command \"coldWhite\"\n        command \"warmWhite\"\n\n        // Program commands:\n        command \"startProgram\"\n        command \"stopProgram\"\n        command \"startFireplace\"\n        command \"startStorm\"\n        command \"startDeepFade\"\n        command \"startLiteFade\"\n        command \"startPolice\"\n\n        fingerprint deviceId: \"0x1101\", inClusters: \"0x27,0x72,0x86,0x26,0x60,0x70,0x32,0x31,0x85,0x33\"\n    }\n\n    tiles (scale: 2){\n        // MultiTile:\n        multiAttributeTile(name:\"switch\", type: \"lighting\", width: 6, height: 4, canChangeIcon: true){\n            tileAttribute (\"device.switch\", key: \"PRIMARY_CONTROL\") {\n                attributeState \"on\", label:'${name}', action:\"switch.off\", icon:\"st.lights.philips.hue-single\", backgroundColor:\"#79B821\", nextState:\"turningOff\"\n                attributeState \"off\", label:'${name}', action:\"switch.on\", icon:\"st.lights.philips.hue-single\", backgroundColor:\"#ffffff\", nextState:\"turningOn\"\n                attributeState \"turningOn\", label:'${name}', action:\"switch.off\", icon:\"st.lights.philips.hue-single\", backgroundColor:\"#79B821\", nextState:\"turningOff\"\n                attributeState \"turningOff\", label:'${name}', action:\"switch.on\", icon:\"st.lights.philips.hue-single\", backgroundColor:\"#ffffff\", nextState:\"turningOn\"\n            }\n            tileAttribute (\"device.level\", key: \"SLIDER_CONTROL\", range:\"(0..500)\") {\n                attributeState \"level\", action:\"setLevel\"\n            }\n            tileAttribute (\"device.color\", key: \"COLOR_CONTROL\") {\n                attributeState \"color\", action:\"setColor\"\n            }\n            tileAttribute (\"device.power\", key: \"SECONDARY_CONTROL\") {\n                attributeState \"power\", label:'${currentValue} W'\n            }\n        }\n\n        // Colour Channels:\n        standardTile(\"switchRed\", \"device.switchRed\", height: 1, width: 1, inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"R\", action:\"onRed\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\"\n            state \"on\", label:\"R\", action:\"offRed\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF0000\"\n        }\n        controlTile(\"levelRedSlider\", \"device.levelRed\", \"slider\", range:\"(0..100)\", height: 1, width: 4, inactiveLabel: false) {\n            state \"levelRed\", action:\"setLevelRed\"\n        }\n        valueTile(\"levelRedTile\", \"device.levelRed\", decoration: \"flat\", height: 1, width: 1) {\n            state \"levelRed\", label:'${currentValue}%'\n        }\n\n        standardTile(\"switchGreen\", \"device.switchGreen\", height: 1, width: 1, inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"G\", action:\"onGreen\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\"\n            state \"on\", label:\"G\", action:\"offGreen\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#00FF00\"\n        }\n        controlTile(\"levelGreenSlider\", \"device.levelGreen\", \"slider\", range:\"(0..100)\", height: 1, width: 4, inactiveLabel: false) {\n            state \"levelGreen\", action:\"setLevelGreen\"\n        }\n        valueTile(\"levelGreenTile\", \"device.levelGreen\", decoration: \"flat\", height: 1, width: 1) {\n            state \"levelGreen\", label:'${currentValue}%'\n        }\n\n        standardTile(\"switchBlue\", \"device.switchBlue\", height: 1, width: 1, inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"B\", action:\"onBlue\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\"\n            state \"on\", label:\"B\", action:\"offBlue\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#0000FF\"\n        }\n        controlTile(\"levelBlueSlider\", \"device.levelBlue\", \"slider\", range:\"(0..100)\", height: 1, width: 4, inactiveLabel: false) {\n            state \"levelBlue\", action:\"setLevelBlue\"\n        }\n        valueTile(\"levelBlueTile\", \"device.levelBlue\", decoration: \"flat\", height: 1, width: 1) {\n            state \"levelBlue\", label:'${currentValue}%'\n        }\n\n        standardTile(\"switchWhite\", \"device.switchWhite\", height: 1, width: 1, inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"W\", action:\"onWhite\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\"\n            state \"on\", label:\"W\", action:\"offWhite\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FFFFFF\"\n        }\n        controlTile(\"levelWhiteSlider\", \"device.levelWhite\", \"slider\", range:\"(0..100)\", height: 1, width: 4, inactiveLabel: false) {\n            state \"levelWhite\", action:\"setLevelWhite\"\n        }\n        valueTile(\"levelWhiteTile\", \"device.levelWhite\", decoration: \"flat\", height: 1, width: 1) {\n            state \"levelWhite\", label:'${currentValue}%'\n        }\n\n        // OUT Channels:\n        standardTile(\"switchCh1\", \"device.switchCh1\", height: 1, width: 1, inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"1\", action:\"onCh1\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\"\n            state \"on\", label:\"1\", action:\"offCh1\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#79B821\"\n        }\n        controlTile(\"levelCh1Slider\", \"device.levelCh1\", \"slider\", range:\"(0..100)\", height: 1, width: 4, inactiveLabel: false) {\n            state \"levelCh1\", action:\"setLevelCh1\"\n        }\n        valueTile(\"levelCh1Tile\", \"device.levelCh1\", decoration: \"flat\", height: 1, width: 1) {\n            state \"levelCh1\", label:'${currentValue}%'\n        }\n\n        standardTile(\"switchCh2\", \"device.switchCh2\", height: 1, width: 1, inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"2\", action:\"onCh2\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\"\n            state \"on\", label:\"2\", action:\"offCh2\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#79B821\"\n        }\n        controlTile(\"levelCh2Slider\", \"device.levelCh2\", \"slider\", range:\"(0..100)\", height: 1, width: 4, inactiveLabel: false) {\n            state \"levelCh2\", action:\"setLevelCh2\"\n        }\n        valueTile(\"levelCh2Tile\", \"device.levelCh2\", decoration: \"flat\", height: 1, width: 1) {\n            state \"levelCh2\", label:'${currentValue}%'\n        }\n\n        standardTile(\"switchCh3\", \"device.switchCh3\", height: 1, width: 1, inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"3\", action:\"onCh3\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\"\n            state \"on\", label:\"3\", action:\"offCh3\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#79B821\"\n        }\n        controlTile(\"levelCh3Slider\", \"device.levelCh3\", \"slider\", range:\"(0..100)\", height: 1, width: 4, inactiveLabel: false) {\n            state \"levelCh3\", action:\"setLevelCh3\"\n        }\n        valueTile(\"levelCh3Tile\", \"device.levelCh3\", decoration: \"flat\", height: 1, width: 1) {\n            state \"levelCh3\", label:'${currentValue}%'\n        }\n\n        standardTile(\"switchCh4\", \"device.switchCh4\", height: 1, width: 1, inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"4\", action:\"onCh4\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\"\n            state \"on\", label:\"4\", action:\"offCh4\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#79B821\"\n        }\n        controlTile(\"levelCh4Slider\", \"device.levelCh4\", \"slider\", range:\"(0..100)\", height: 1, width: 4, inactiveLabel: false) {\n            state \"levelCh4\", action:\"setLevelCh4\"\n        }\n        valueTile(\"levelCh4Tile\", \"device.levelCh4\", decoration: \"flat\", height: 1, width: 1) {\n            state \"levelCh4\", label:'${currentValue}%'\n        }\n\n        // IN Channels (READ-ONLY) Labels:\n        valueTile(\"switchCh1ReadOnly\", \"device.switchCh1\", decoration: \"flat\", height: 1, width: 1) {\n            state \"default\", label:'${currentValue}'\n        }\n        valueTile(\"ch1Label\", \"device.switchCh1\", decoration: \"flat\", height: 1, width: 4) {\n            state \"default\", label:'Channel #1 (Input):'\n        }\n\n        valueTile(\"switchCh2ReadOnly\", \"device.switchCh2\", decoration: \"flat\", height: 1, width: 1) {\n            state \"default\", label:'${currentValue}'\n        }\n        valueTile(\"ch2Label\", \"device.switchCh1\", decoration: \"flat\", height: 1, width: 4) {\n            state \"default\", label:'Channel #2 (Input):'\n        }\n\n        valueTile(\"switchCh3ReadOnly\", \"device.switchCh3\", decoration: \"flat\", height: 1, width: 1) {\n            state \"default\", label:'${currentValue}'\n        }\n        valueTile(\"ch3Label\", \"device.switchCh1\", decoration: \"flat\", height: 1, width: 4) {\n            state \"default\", label:'Channel #3 (Input):'\n        }\n\n        valueTile(\"switchCh4ReadOnly\", \"device.switchCh4\", decoration: \"flat\", height: 1, width: 1) {\n            state \"default\", label:'${currentValue}'\n        }\n        valueTile(\"ch4Label\", \"device.switchCh1\", decoration: \"flat\", height: 1, width: 4) {\n            state \"default\", label:'Channel #4 (Input):'\n        }\n\n        // Power\n        valueTile(\"powerLabel\", \"device.power\", decoration: \"flat\", height: 1, width: 2) {\n            state \"default\", label:'Power:', action:\"refresh.refresh\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_refresh.png\"\n        }\n        valueTile(\"power\", \"device.power\", decoration: \"flat\", height: 1, width: 2) {\n            state \"power\", label:'${currentValue} W'\n        }\n\n        // Energy:\n        valueTile(\"lastReset\", \"device.lastReset\", decoration: \"flat\", height: 1, width: 2) {\n            state \"default\", label:'Since:  ${currentValue}', action:\"reset\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_stopwatch_reset.png\"\n        }\n        valueTile(\"energy\", \"device.energy\", height: 1, width: 2) {\n            state \"default\", label:'${currentValue} kWh', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n        }\n\n        // Programs:\n        standardTile(\"fireplace\", \"device.activeProgram\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"Fireplace\", action:\"startFireplace\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"6\", label:\"Fireplace\", action:\"stopProgram\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF0000\"\n        }\n        standardTile(\"storm\", \"device.activeProgram\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"storm\", action:\"startStorm\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"7\", label:\"storm\", action:\"stopProgram\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF0000\"\n        }\n        standardTile(\"deepFade\", \"device.activeProgram\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"deep fade\", action:\"startDeepFade\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"8\", label:\"deep fade\", action:\"stopProgram\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF0000\"\n        }\n        standardTile(\"liteFade\", \"device.activeProgram\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"lite fade\", action:\"startLiteFade\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"9\", label:\"lite fade\", action:\"stopProgram\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF0000\"\n        }\n        standardTile(\"police\", \"device.activeProgram\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"police\", action:\"startPolice\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"10\", label:\"police\", action:\"stopProgram\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF0000\"\n        }\n\n        // Colour Shortcuts:\n        standardTile(\"red\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"red\", action:\"red\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"red\", label:\"red\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF0000\"\n        }\n        standardTile(\"green\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"green\", action:\"green\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"green\", label:\"green\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#00FF00\"\n        }\n        standardTile(\"blue\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"blue\", action:\"blue\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"blue\", label:\"blue\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#0000FF\"\n        }\n        standardTile(\"cyan\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"cyan\", action:\"cyan\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"cyan\", label:\"cyan\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#00FFFF\"\n        }\n        standardTile(\"magenta\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"magenta\", action:\"magenta\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"magenta\", label:\"magenta\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF0040\"\n        }\n        standardTile(\"orange\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"orange\", action:\"orange\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"orange\", label:\"orange\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF6600\"\n        }\n        standardTile(\"purple\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"purple\", action:\"purple\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"purple\", label:\"purple\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#BF00FF\"\n        }\n        standardTile(\"yellow\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"yellow\", action:\"yellow\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"yellow\", label:\"yellow\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FFFF00\"\n        }\n        standardTile(\"pink\", \"device.colorName\", height: 2, width: 2, decoration: \"flat\", inactiveLabel: false, canChangeIcon: false) {\n            state \"off\", label:\"pink\", action:\"pink\", icon:\"st.illuminance.illuminance.dark\", backgroundColor:\"#D8D8D8\", defaultState: true\n            state \"pink\", label:\"pink\", action:\"off\", icon:\"st.illuminance.illuminance.bright\", backgroundColor:\"#FF33CB\"\n        }\n\n        standardTile(\"refresh\", \"device.switch\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:\"\", action:\"refresh.refresh\", icon:\"st.secondary.refresh\"\n        }\n        standardTile(\"test\", \"device.switch\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Test', action:\"test\"\n        }\n\n        // Tile layouts:\n        // ******** EDIT THIS SECTION to show the Tiles you want ********\n        main([\"switch\"])\n        details([\n            // The main multitile:\n            \"switch\",\n\n            // RGBW Channels:\n            \"switchRed\",\"levelRedSlider\", \"levelRedTile\",\n            \"switchGreen\",\"levelGreenSlider\", \"levelGreenTile\",\n            \"switchBlue\",\"levelBlueSlider\", \"levelBlueTile\",\n            \"switchWhite\",\"levelWhiteSlider\", \"levelWhiteTile\",\n\n            // OUT Channels:\n            //\"switchCh1\",\"levelCh1Slider\", \"levelCh1Tile\",\n            //\"switchCh2\",\"levelCh2Slider\", \"levelCh2Tile\",\n            //\"switchCh3\",\"levelCh3Slider\", \"levelCh3Tile\",\n            //\"switchCh4\",\"levelCh4Slider\", \"levelCh4Tile\",\n\n            // INPUT Channels (read-only, label replaced slider control):\n            //\"switchCh1ReadOnly\", \"ch1Label\", \"levelCh1Tile\",\n            //\"switchCh2ReadOnly\", \"ch2Label\", \"levelCh2Tile\",\n            //\"switchCh3ReadOnly\", \"ch3Label\", \"levelCh3Tile\",\n            //\"switchCh4ReadOnly\", \"ch4Label\", \"levelCh4Tile\",\n\n            // Energy and Power:\n            \"powerLabel\", \"power\", \"refresh\", \"lastReset\", \"energy\",\n\n            // Built-in Program Shortcuts (these only work if the channels are RGBW):\n            \"fireplace\", \"storm\", \"deepFade\",\"liteFade\", \"police\",\n\n            // Color Shortcut Tiles (these only work if channels are mapped to red/green/blue/white):\n            \"red\",\"green\",\"blue\",\n            \"orange\",\"yellow\",\"cyan\",\n            \"magenta\",\"pink\",\"purple\",\n\n            // The Test Tile:\n            //\"test\"\n            ])\n    }\n\n    preferences {\n        section { // GENERAL:\n            input type: \"paragraph\", element: \"paragraph\",\n                title: \"GENERAL:\", description: \"General settings.\"\n\n            input name: \"configDebugMode\", type: \"boolean\", defaultValue: false, displayDuringSetup: false,\n                title: \"Enable debug logging?\"\n\n        }\n\n        section { // AGGREGATE SWITCH/LEVEL:\n            input type: \"paragraph\", element: \"paragraph\",\n                title: \"AGGREGATE SWITCH/LEVEL:\", description: \"These settings control how the device's 'switch' and 'level' attributes are calculated.\"\n\n            input name: \"configAggregateSwitchMode\", type: \"enum\", defaultValue: \"OUT\", required: true, displayDuringSetup: false,\n                title: \"Calaculate Aggregate 'switch' value from:\\n[Default: RBGW/OUT Channels Only]\",\n                options: [\"OUT\" : \"RBGW/OUT Channels Only\",\n                          \"IN\" : \"IN Channels Only\",\n                          \"ALL\" : \"All Channels\"]\n\n            input name: \"configAggregateLevelMode\", type: \"enum\", defaultValue: \"OUT\", required: true, displayDuringSetup: false,\n                title: \"Calaculate Aggregate 'level' value from:\\n[Default: RBGW/OUT Channels Only]\",\n                options: [\"OUT\" : \"RBGW/OUT Channels Only\",\n                          \"IN\" : \"IN Channels Only\",\n                          \"ALL\" : \"All Channels\"]\n\n            input name: \"configLevelSetMode\", type: \"enum\", defaultValue: \"SCALE\", required: true, displayDuringSetup: false,\n                title: \"LEVEL SET Mode:\\n[Default: SCALE]\",\n                options: [\"SCALE\" : \"SCALE individual channel levels\",\n                          \"SIMPLE\" : \"SIMPLE: Set all channels to new level\"]\n\n        }\n\n        section { // CHANNEL MAPPING & THRESHOLDS:\n            input type: \"paragraph\", element: \"paragraph\",\n                title: \"CHANNEL MAPPING & THRESHOLDS:\", description: \"Define how the physical channels map to colours.\\n\" +\n                       \"Thresholds define the level at which a channel is considered ON, which can be used to translate an analog input to a binary value.\"\n\n            input name: \"configCh1Mapping\", type: \"enum\", defaultValue: \"Red\", required: true, displayDuringSetup: false,\n                title: \"Channel #1: Maps to:\",\n                options: [\"Red\" : \"Red\",\n                          \"Green\" : \"Green\",\n                          \"Blue\" : \"Blue\",\n                          \"White\" : \"White\",\n                          \"Other\" : \"Other\",\n                          \"Input\" : \"Input\"]\n\n            input name: \"configCh1Threshold\", type: \"number\", range: \"0..100\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"Channel #1: Threshold for ON (%):\"\n\n            input name: \"configCh2Mapping\", type: \"enum\", defaultValue: \"Green\", required: true, displayDuringSetup: false,\n                title: \"Channel #2: Maps to:\",\n                options: [\"Red\" : \"Red\",\n                          \"Green\" : \"Green\",\n                          \"Blue\" : \"Blue\",\n                          \"White\" : \"White\",\n                          \"Other\" : \"Other\",\n                          \"Input\" : \"Input\"]\n\n            input name: \"configCh2Threshold\", type: \"number\", range: \"0..100\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"Channel #2: Threshold for ON (%):\"\n\n            input name: \"configCh3Mapping\", type: \"enum\", defaultValue: \"Blue\", required: true, displayDuringSetup: false,\n                title: \"Channel #3: Maps to:\",\n                options: [\"Red\" : \"Red\",\n                          \"Green\" : \"Green\",\n                          \"Blue\" : \"Blue\",\n                          \"White\" : \"White\",\n                          \"Other\" : \"Other\",\n                          \"Input\" : \"Input\"]\n\n            input name: \"configCh3Threshold\", type: \"number\", range: \"0..100\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"Channel #3: Threshold for ON (%):\"\n\n            input name: \"configCh4Mapping\", type: \"enum\", defaultValue: \"White\", required: true, displayDuringSetup: false,\n                title: \"Channel #4: Maps to:\",\n                options: [\"Red\" : \"Red\",\n                          \"Green\" : \"Green\",\n                          \"Blue\" : \"Blue\",\n                          \"White\" : \"White\",\n                          \"Other\" : \"Other\",\n                          \"Input\" : \"Input\"]\n\n            input name: \"configCh4Threshold\", type: \"number\", range: \"0..100\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"Channel #4: Threshold for ON (%):\"\n\n        }\n\n        section { // PHYSICAL DEVICE PARAMETERS:\n            input type: \"paragraph\", element: \"paragraph\",\n                title: \"PHYSICAL DEVICE PARAMETERS:\", description: \"Refer to the Fibaro manual for a full description of the device parameters.\"\n\n            input name: \"configParam01\", type: \"enum\", defaultValue: \"255\", required: true, displayDuringSetup: false,\n                title: \"#1: ALL ON/ALL OFF function:\\n[Default: 255]\",\n                options: [\"0\" : \"0: ALL ON inactive, ALL OFF inactive\",\n                          \"1\" : \"1: ALL ON inactive, ALL OFF active\",\n                          \"2\" : \"2: ALL ON active, ALL OFF inactive\",\n                          \"255\" : \"255: ALL ON active, ALL OFF active\"]\n\n            input name: \"configParam06\", type: \"enum\", defaultValue: \"0\", required: true, displayDuringSetup: false,\n                title: \"#6: Associations command class:\\n[Default: 0]\",\n                options: [\"0\" : \"0: NORMAL (DIMMER) - BASIC SET/SWITCH_MULTILEVEL_START/STOP\",\n                          \"1\" : \"1: NORMAL (RGBW) - COLOR_CONTROL_SET/START/STOP_STATE_CHANGE\",\n                          \"2\" : \"2: NORMAL (RGBW) - COLOR_CONTROL_SET\",\n                          \"3\" : \"3: BRIGHTNESS - BASIC SET/SWITCH_MULTILEVEL_START/STOP\",\n                          \"4\" : \"4: RAINBOW (RGBW) - COLOR_CONTROL_SET\"]\n\n            input name: \"configParam08\", type: \"enum\", defaultValue: \"0\", required: true, displayDuringSetup: false,\n                title: \"#8: IN/OUT: Outputs state change mode:\\n[Default: 0: MODE1]\",\n                options: [\"0\" : \"0: MODE1\",\n                          \"1\" : \"1: MODE2\"]\n\n            input name: \"configParam09\", type: \"number\", range: \"1..255\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"#9: MODE1: Step value:\\n[Default: 1]\"\n\n            input name: \"configParam10\", type: \"number\", range: \"0..60000\", defaultValue: \"10\", required: true, displayDuringSetup: false,\n                title: \"#10: MODE1: Time between steps:\\n[Default: 10ms]\\n\" +\n                       \" - 0: immediate change\"\n\n            input name: \"configParam11\", type: \"number\", range: \"0..255\", defaultValue: \"67\", required: true, displayDuringSetup: false,\n                title: \"#11: MODE2: Time for changing from start to end value:\\n\" +\n                       \"[Default: 67 = 3s]\\n\" +\n                       \" - 0: immediate change\\n\" +\n                       \" - 1-63: 20-126- [ms] value*20ms\\n\" +\n                       \" - 65-127: 1-63 [s] [value-64]*1s\\n\" +\n                       \" - 129-191: 10-630[s] [value-128]*10s\\n\" +\n                       \" - 193-255: 1-63[min] [value-192]*1min\"\n\n            input name: \"configParam12\", type: \"number\", range: \"3..255\", defaultValue: \"255\", required: true, displayDuringSetup: false,\n                title: \"#12: Maximum brightening level:\\n[Default: 255]\"\n\n            input name: \"configParam13\", type: \"number\", range: \"0..254\", defaultValue: \"2\", required: true, displayDuringSetup: false,\n                title: \"#13: Minimum dim level:\\n[Default: 2]\"\n\n            input type: \"paragraph\", element: \"paragraph\",\n                title: \"#14: IN/OUT Channel settings: \", description: \"If RGBW mode is chosen, settings for all 4 channels must be identical.\"\n\n            input name: \"configParam14_1\", type: \"enum\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"CHANNEL 1:\\n[Default: 1: RGBW - MOMENTARY (NORMAL MODE)]\",\n                options: [\"1\" : \"1: RGBW - MOMENTARY (NORMAL MODE)\",\n                          \"2\" : \"2: RGBW - MOMENTARY (BRIGHTNESS MODE)\",\n                          \"3\" : \"3: RGBW - MOMENTARY (RAINBOW MODE)\",\n                          \"4\" : \"4: RGBW - TOGGLE (NORMAL MODE)\",\n                          \"5\" : \"5: RGBW - TOGGLE (BRIGHTNESS MODE)\",\n                          \"6\" : \"6: RGBW - TOGGLE W. MEMORY (NORMAL MODE)\",\n                          \"7\" : \"7: RGBW - TOGGLE W. MEMORY (BRIGHTNESS MODE)\",\n                          \"8\" : \"8: IN - ANALOG 0-10V (SENSOR)\",\n                          \"9\" : \"9: OUT - MOMENTARY (NORMAL MODE)\",\n                          \"12\" : \"12: OUT - TOGGLE (NORMAL MODE)\",\n                          \"14\" : \"14: OUT - TOGGLE W. MEMORY (NORMAL MODE)\"]\n\n            input name: \"configParam14_2\", type: \"enum\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"CHANNEL 2:\\n[Default: 1: RGBW - MOMENTARY (NORMAL MODE)]\",\n                options: [\"1\" : \"1: RGBW - MOMENTARY (NORMAL MODE)\",\n                          \"2\" : \"2: RGBW - MOMENTARY (BRIGHTNESS MODE)\",\n                          \"3\" : \"3: RGBW - MOMENTARY (RAINBOW MODE)\",\n                          \"4\" : \"4: RGBW - TOGGLE (NORMAL MODE)\",\n                          \"5\" : \"5: RGBW - TOGGLE (BRIGHTNESS MODE)\",\n                          \"6\" : \"6: RGBW - TOGGLE W. MEMORY (NORMAL MODE)\",\n                          \"7\" : \"7: RGBW - TOGGLE W. MEMORY (BRIGHTNESS MODE)\",\n                          \"8\" : \"8: IN - ANALOG 0-10V (SENSOR)\",\n                          \"9\" : \"9: OUT - MOMENTARY (NORMAL MODE)\",\n                          \"12\" : \"12: OUT - TOGGLE (NORMAL MODE)\",\n                          \"14\" : \"14: OUT - TOGGLE W. MEMORY (NORMAL MODE)\"]\n\n            input name: \"configParam14_3\", type: \"enum\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"CHANNEL 3:\\n[Default: 1: RGBW - MOMENTARY (NORMAL MODE)]\",\n                options: [\"1\" : \"1: RGBW - MOMENTARY (NORMAL MODE)\",\n                          \"2\" : \"2: RGBW - MOMENTARY (BRIGHTNESS MODE)\",\n                          \"3\" : \"3: RGBW - MOMENTARY (RAINBOW MODE)\",\n                          \"4\" : \"4: RGBW - TOGGLE (NORMAL MODE)\",\n                          \"5\" : \"5: RGBW - TOGGLE (BRIGHTNESS MODE)\",\n                          \"6\" : \"6: RGBW - TOGGLE W. MEMORY (NORMAL MODE)\",\n                          \"7\" : \"7: RGBW - TOGGLE W. MEMORY (BRIGHTNESS MODE)\",\n                          \"8\" : \"8: IN - ANALOG 0-10V (SENSOR)\",\n                          \"9\" : \"9: OUT - MOMENTARY (NORMAL MODE)\",\n                          \"12\" : \"12: OUT - TOGGLE (NORMAL MODE)\",\n                          \"14\" : \"14: OUT - TOGGLE W. MEMORY (NORMAL MODE)\"]\n\n            input name: \"configParam14_4\", type: \"enum\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"CHANNEL 4:\\n[Default: 1: RGBW - MOMENTARY (NORMAL MODE)]\",\n                options: [\"1\" : \"1: RGBW - MOMENTARY (NORMAL MODE)\",\n                          \"2\" : \"2: RGBW - MOMENTARY (BRIGHTNESS MODE)\",\n                          \"3\" : \"3: RGBW - MOMENTARY (RAINBOW MODE)\",\n                          \"4\" : \"4: RGBW - TOGGLE (NORMAL MODE)\",\n                          \"5\" : \"5: RGBW - TOGGLE (BRIGHTNESS MODE)\",\n                          \"6\" : \"6: RGBW - TOGGLE W. MEMORY (NORMAL MODE)\",\n                          \"7\" : \"7: RGBW - TOGGLE W. MEMORY (BRIGHTNESS MODE)\",\n                          \"8\" : \"8: IN - ANALOG 0-10V (SENSOR)\",\n                          \"9\" : \"9: OUT - MOMENTARY (NORMAL MODE)\",\n                          \"12\" : \"12: OUT - TOGGLE (NORMAL MODE)\",\n                          \"14\" : \"14: OUT - TOGGLE W. MEMORY (NORMAL MODE)\"]\n\n            input name: \"configParam16\", type: \"enum\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"#16: Memorise device status at power cut:\\n[Default: 1: MEMORISE STATUS]\",\n                options: [\"0\" : \"0: DO NOT MEMORISE STATUS\",\n                          \"1\" : \"1: MEMORISE STATUS\"]\n\n            input name: \"configParam30\", type: \"enum\", defaultValue: \"0\", required: true, displayDuringSetup: false,\n                title: \"#30: Response to ALARM of any type:\\n[Default: 0: INACTIVE]\",\n                options: [\"0\" : \"0: INACTIVE - Device doesn't respond\",\n                          \"1\" : \"1: ALARM ON - Device turns on when alarm is detected\",\n                          \"2\" : \"2: ALARM OFF - Device turns off when alarm is detected\",\n                          \"3\" : \"3: ALARM PROGRAM - Alarm sequence turns on (Parameter #38)\"]\n\n            input name: \"configParam38\", type: \"number\", range: \"1..10\", defaultValue: \"10\", required: true, displayDuringSetup: false,\n                title: \"#38: Alarm sequence program:\\n[Default: 10]\"\n\n            input name: \"configParam39\", type: \"number\", range: \"1..65534\", defaultValue: \"600\", required: true, displayDuringSetup: false,\n                title: \"#39: Active PROGRAM alarm time:\\n[Default: 600s]\"\n\n            input name: \"configParam42\", type: \"enum\", defaultValue: \"0\", required: true, displayDuringSetup: false,\n                title: \"#42: Command class reporting outputs status change:\\n[Default: 0]\",\n                options: [\"0\" : \"0: Reporting as a result of inputs and controllers actions (SWITCHMULTILEVEL)\",\n                          \"1\" : \"1: Reporting as a result of input actions (SWITCH MULTILEVEL)\",\n                          \"2\" : \"2: Reporting as a result of input actions (COLOR CONTROL)\"]\n\n            input name: \"configParam43\", type: \"number\", range: \"1..100\", defaultValue: \"5\", required: true, displayDuringSetup: false,\n                title: \"#43: Reporting 0-10v analog inputs change threshold:\\n[Default: 5 = 0.5V]\"\n\n            input name: \"configParam44\", type: \"number\", range: \"0..65534\", defaultValue: \"30\", required: true, displayDuringSetup: false,\n                title: \"#44: Power load reporting frequency:\\n[Default: 30s]\\n\" +\n                       \" - 0: reports are not sent\\n\" +\n                       \" - 1-65534: time between reports (s)\"\n\n            input name: \"configParam45\", type: \"number\", range: \"0..254\", defaultValue: \"10\", required: true, displayDuringSetup: false,\n                title: \"#45: Reporting changes in energy:\\n[Default: 10 = 0.1kWh]\\n\" +\n                       \" - 0: reports are not sent\\n\" +\n                       \" - 1-254: 0.01kWh - 2.54kWh\"\n\n            input name: \"configParam71\", type: \"enum\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"#71: Response to BRIGHTNESS set to 0%:\\n[Default: 1]\",\n                options: [\"0\" : \"0: Illumination colour set to white\",\n                          \"1\" : \"1: Last set colour is memorised\"]\n\n            input name: \"configParam72\", type: \"number\", range: \"1..10\", defaultValue: \"1\", required: true, displayDuringSetup: false,\n                title: \"#72: Start predefined (RGBW) program:\\n[Default: 1]\\n\" +\n                       \" - 1-10: animation program number\"\n\n            input name: \"configParam73\", type: \"enum\", defaultValue: \"0\", required: true, displayDuringSetup: false,\n                title: \"#73: Triple click action:\\n[Default: 0]\",\n                options: [\"0\" : \"0: NODE INFO control frame is sent\",\n                          \"1\" : \"1: Start favourite program\"]\n\n        }\n\n        section { // ASSOCIATION GROUPS:\n            input type: \"paragraph\", element: \"paragraph\",\n                title: \"ASSOCIATION GROUPS:\", description: \"Enter a comma-delimited list of node IDs for each association group.\\n\" +\n                    \"Node IDs must be in decimal format (E.g.: 27,155, ... ).\\n\" +\n                    \"Each group allows a maximum of five devices.\\n\"\n\n            input name: \"configAssocGroup01\", type: \"text\", defaultValue: \"\", displayDuringSetup: false,\n                title: \"Association Group #1:\"\n\n            input name: \"configAssocGroup02\", type: \"text\", defaultValue: \"\", displayDuringSetup: false,\n                title: \"Association Group #2:\"\n\n            input name: \"configAssocGroup03\", type: \"text\", defaultValue: \"\", displayDuringSetup: false,\n                title: \"Association Group #3:\"\n\n            input name: \"configAssocGroup04\", type: \"text\", defaultValue: \"\", displayDuringSetup: false,\n                title: \"Association Group #4:\"\n\n        }\n\n    }\n}\n\n/**********************************************************************\n *  Z-wave Event Handlers.\n **********************************************************************/\n\n/**\n *  parse() - Called when messages from a device are received by the hub.\n *\n *  The parse method is responsible for interpreting those messages and returning Event definitions.\n *\n *  String      description         - The message from the device.\n **/\ndef parse(description) {\n    if (state.debug) log.trace \"${device.displayName}: parse(): Parsing raw message: ${description}\"\n\n    def result = null\n    if (description != \"updated\") {\n        def cmd = zwave.parse(description, getSupportedCommands())\n        if (cmd) {\n            result = zwaveEvent(cmd)\n        } else {\n            log.error \"${device.displayName}: parse(): Could not parse raw message: ${description}\"\n        }\n    }\n    return result\n}\n\n/**\n *  COMMAND_CLASS_BASIC (0x20) : BasicReport [IGNORED]\n *\n *  Short   value   0xFF for on, 0x00 for off\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): BasicReport received: ${cmd}\"\n    // BasicReports are ignored as the aggregate switch and level attributes are calculated seperately.\n}\n\n/**\n *  COMMAND_CLASS_SWITCH_MULTILEVEL (0x26) : SwitchMultilevelReport\n *\n *  SwitchMultilevelReports tell us the current level of a channel.\n *\n *  These reports will arrive via a MultiChannelCmdEncap command, the zwaveEvent(...MultiChannelCmdEncap) handler\n *  will add the correct sourceEndPoint, before passing to this event handler.\n *\n *  Fibaro RGBW SwitchMultilevelReports have value in range [0..99], so this is scaled to 255 and passed to\n *  zwaveEndPointEvent().\n *\n *  Short       value       0x00 for off, other values are level (on).\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv2.SwitchMultilevelReport cmd, sourceEndPoint = 0) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): SwitchMultilevelReport received from endPoint ${sourceEndPoint}: ${cmd}\"\n    return zwaveEndPointEvent(sourceEndPoint, Math.round(cmd.value * 255/99))\n}\n\n/**\n *  COMMAND_CLASS_SWITCH_ALL (0x27) : * [IGNORED]\n *\n *  SwitchAll functionality is controlled and reported via device Parameter #1 instead.\n **/\n\n/**\n *  COMMAND_CLASS_SENSOR_MULTILEVEL (0x31) : SensorMultilevelReport\n *\n *  Appears to be used to report power. Not sure if anything else...?\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): SensorMultilevelReport received: ${cmd}\"\n\n    if ( cmd.sensorType == 4 ) { // Instantaneous Power (Watts):\n        log.info \"${device.displayName}: Power is ${cmd.scaledSensorValue} W\"\n        return createEvent(name: \"power\", value: cmd.scaledSensorValue, unit: \"W\")\n    }\n    else {\n        log.warn \"${device.displayName}: zwaveEvent(): SensorMultilevelReport with unhandled sensorType: ${cmd}\"\n    }\n}\n\n/**\n *  COMMAND_CLASS_METER_V3 (0x32) : MeterReport\n *\n *  The Fibaro RGBW Controller supports scale 0 (energy), and 2 (power) only.\n *\n *  Integer         deltaTime                   Time in seconds since last report\n *  Short           meterType                   Unknown = 0, Electric = 1, Gas = 2, Water = 3\n *  List<Short>     meterValue                  Meter value as an array of bytes\n *  Double          scaledMeterValue            Meter value as a double\n *  List<Short>     previousMeterValue          Previous meter value as an array of bytes\n *  Double          scaledPreviousMeterValue    Previous meter value as a double\n *  Short           size                        The size of the array for the meterValue and previousMeterValue\n *  Short           scale                       The scale of the values: \"kWh\"=0, \"kVAh\"=1, \"Watts\"=2, \"pulses\"=3,\n *                                              \"Volts\"=4, \"Amps\"=5, \"Power Factor\"=6, \"Unknown\"=7\n *  Short           precision                   The decimal precision of the values\n *  Short           rateType                    ???\n *  Boolean         scale2                      ???\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): MeterReport received: ${cmd}\"\n\n    if (cmd.scale == 0) { // Accumulated Energy (kWh):\n        state.energy = cmd.scaledMeterValue\n        //sendEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" kWh\", displayed: false)\n        log.info \"${device.displayName}: Accumulated energy is ${cmd.scaledMeterValue} kWh\"\n        return createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kWh\")\n    }\n    else if (cmd.scale == 1) { // Accumulated Energy (kVAh): Ignore.\n        //createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kVAh\")\n    }\n    else if (cmd.scale == 2) { // Instantaneous Power (Watts):\n        //sendEvent(name: \"dispPower\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" W\", displayed: false)\n        log.info \"${device.displayName}: Power is ${cmd.scaledMeterValue} W\"\n        return createEvent(name: \"power\", value: cmd.scaledMeterValue, unit: \"W\")\n    }\n    else if (cmd.scale == 4) { // Instantaneous Voltage (Volts):\n        //sendEvent(name: \"dispVoltage\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" V\", displayed: false)\n        log.info \"${device.displayName}: Voltage is ${cmd.scaledMeterValue} V\"\n        return createEvent(name: \"voltage\", value: cmd.scaledMeterValue, unit: \"V\")\n    }\n    else if (cmd.scale == 5) {  // Instantaneous Current (Amps):\n        //sendEvent(name: \"dispCurrent\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" A\", displayed: false)\n        log.info \"${device.displayName}: Current is ${cmd.scaledMeterValue} A\"\n        return createEvent(name: \"current\", value: cmd.scaledMeterValue, unit: \"A\")\n    }\n    else if (cmd.scale == 6) { // Instantaneous Power Factor:\n        //sendEvent(name: \"dispPowerFactor\", value: \"PF: \" + String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal), displayed: false)\n        log.info \"${device.displayName}: PowerFactor is ${cmd.scaledMeterValue}\"\n        return createEvent(name: \"powerFactor\", value: cmd.scaledMeterValue, unit: \"PF\")\n    }\n}\n\n/**\n *  COMMAND_CLASS_SWITCH_COLOR (0x33) : SwitchColorReport\n *\n *  SwitchColorReports tell us the current level of a color channel.\n *  The value will be in the range 0..255, which is passed to zwaveEndPointEvent().\n *\n *  String      colorComponent                  Color name, e.g. \"red\", \"green\", \"blue\".\n *  Short       colorComponentId                0 = warmWhite, 2 = red, 3 = green, 4 = blue, 5 = coldWhite.\n *  Short       value                           0x00 to 0xFF\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchcolorv3.SwitchColorReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): SwitchColorReport received: ${cmd}\"\n    if (cmd.colorComponentId == 0) { cmd.colorComponentId = 5 } // Remap warmWhite colorComponentId\n    return zwaveEndPointEvent(cmd.colorComponentId, cmd.value)\n}\n\n/**\n *  COMMAND_CLASS_MULTICHANNEL (0x60) : MultiChannelCmdEncap\n *\n *  The MultiChannel Command Class is used to address one or more endpoints in a multi-channel device.\n *  The sourceEndPoint attribute will identify the sub-device/channel the command relates to.\n *  The encpsulated command is extracted and passed to the appropriate zwaveEvent handler.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): MultiChannelCmdEncap received: ${cmd}\"\n\n    def encapsulatedCommand = cmd.encapsulatedCommand(getSupportedCommands())\n    if (!encapsulatedCommand) {\n        log.warn \"${device.displayName}: zwaveEvent(): MultiChannelCmdEncap from endPoint ${cmd.sourceEndPoint} could not be translated: ${cmd}\"\n    } else {\n        return zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint)\n    }\n}\n\n/**\n *  COMMAND_CLASS_CONFIGURATION (0x70) : ConfigurationReport\n *\n *  Configuration reports tell us the current parameter values stored in the physical device.\n *\n *  Due to platform security restrictions, the relevent preference value cannot be updated with the actual\n *  value from the device, instead all we can do is output to the SmartThings IDE Log for verification.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): ConfigurationReport received: ${cmd}\"\n    // Translate cmd.configurationValue to an int. This should be returned from zwave.parse() as\n    // cmd.scaledConfigurationValue, but it hasn't been implemented by SmartThings yet! :/\n    //  See: https://community.smartthings.com/t/zwave-configurationv2-configurationreport-dev-question/9771\n    def scaledConfigurationValue = byteArrayToInt(cmd.configurationValue)\n    log.info \"${device.displayName}: Parameter #${cmd.parameterNumber} has value: ${cmd.configurationValue} (${scaledConfigurationValue})\"\n}\n\n/**\n *  COMMAND_CLASS_MANUFACTURER_SPECIFIC (0x72) : ManufacturerSpecificReport\n *\n *  ManufacturerSpecific reports tell us the device's manufacturer ID and product ID.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): ManufacturerSpecificReport received: ${cmd}\"\n    updateDataValue(\"manufacturerName\",\"${cmd.manufacturerName}\")\n    updateDataValue(\"manufacturerId\",\"${cmd.manufacturerId}\")\n    updateDataValue(\"productId\",\"${cmd.productId}\")\n    updateDataValue(\"productTypeId\",\"${cmd.productTypeId}\")\n}\n\n\n/**\n *  COMMAND_CLASS_ASSOCIATION (0x85) : AssociationReport\n *\n *  AssociationReports tell the nodes in an association group.\n *  Due to platform security restrictions, the relevent preference value cannot be updated with the actual\n *  value from the device, instead all we can do is output to the SmartThings IDE Log for verification.\n *\n *  Example: AssociationReport(groupingIdentifier: 4, maxNodesSupported: 5, nodeId: [1], reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): AssociationReport received: ${cmd}\"\n    log.info \"${device.displayName}: Association Group ${cmd.groupingIdentifier} contains nodes: ${cmd.nodeId}\"\n}\n\n/**\n *  COMMAND_CLASS_VERSION (0x86) : VersionReport\n *\n *  Version reports tell us the device's Z-Wave framework and firmware versions.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): VersionReport received: ${cmd}\"\n    updateDataValue(\"applicationVersion\",\"${cmd.applicationVersion}\")\n    updateDataValue(\"applicationSubVersion\",\"${cmd.applicationSubVersion}\")\n    updateDataValue(\"zWaveLibraryType\",\"${cmd.zWaveLibraryType}\")\n    updateDataValue(\"zWaveProtocolVersion\",\"${cmd.zWaveProtocolVersion}\")\n    updateDataValue(\"zWaveProtocolSubVersion\",\"${cmd.zWaveProtocolSubVersion}\")\n}\n\n/**\n *  COMMAND_CLASS_FIRMWARE_UPDATE_MD (0x7A) : FirmwareMdReport\n *\n *  Firmware Meta Data reports tell us the device's firmware version and manufacturer ID.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEvent(): FirmwareMdReport received: ${cmd}\"\n    updateDataValue(\"firmwareChecksum\",\"${cmd.checksum}\")\n    updateDataValue(\"firmwareId\",\"${cmd.firmwareId}\")\n    updateDataValue(\"manufacturerId\",\"${cmd.manufacturerId}\")\n}\n\n/**\n *  Default zwaveEvent handler.\n *\n *  Called for all Z-Wave events that aren't handled above.\n **/\ndef zwaveEvent(physicalgraph.zwave.Command cmd) {\n    log.error \"${device.displayName}: zwaveEvent(): No handler for command: ${cmd}\"\n    log.error \"${device.displayName}: zwaveEvent(): Class is: ${cmd.getClass()}\" // This causes an error, but still gives us the class in the error message. LOL!\n}\n\n\n/**********************************************************************\n *  SmartThings Platform Commands:\n **********************************************************************/\n\n/**\n *  installed() - Runs when the device is first installed.\n **/\ndef installed() {\n    log.trace \"installed()\"\n\n    state.debug = true\n    state.installedAt = now()\n    state.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    state.channelMapping = [null, \"Red\", \"Green\", \"Blue\", \"White\"]\n    state.channelThresholds = [null,1,1,1,1]\n    state.channelModes = [null,1,1,1,1]\n\n    // Initialise attributes:\n    sendEvent(name: \"switch\", value: \"off\", displayed: false)\n    sendEvent(name: \"level\", value: 0, unit: \"%\", displayed: false)\n    sendEvent(name: \"hue\", value: 0, unit: \"%\", displayed: false)\n    sendEvent(name: \"saturation\", value: 0, unit: \"%\", displayed: false)\n    sendEvent(name: \"colorName\", value: \"custom\", displayed: false)\n    sendEvent(name: \"color\", value: \"[]\", displayed: false)\n    sendEvent(name: \"activeProgram\", value: 0, displayed: false)\n    sendEvent(name: \"energy\", value: 0, unit: \"kWh\", displayed: false)\n    sendEvent(name: \"power\", value: 0, unit: \"W\", displayed: false)\n    sendEvent(name: \"lastReset\", value: state.lastReset, displayed: false)\n\n    (1..4).each { channel ->\n        sendEvent(name: \"switchCh${channel}\", value: \"off\", displayed: false)\n        sendEvent(name: \"levelCh${channel}\", value: 0, unit: \"%\", displayed: false)\n    }\n\n    [\"Red\", \"Green\", \"Blue\", \"White\"].each { mapping ->\n        sendEvent(name: \"switchCh${mapping}\", value: \"off\", displayed: false)\n        sendEvent(name: \"levelCh${mapping}\", value: 0, unit: \"%\", displayed: false)\n    }\n\n    state.isInstalled = true\n}\n\n/**\n *  updated() - Runs after device settings have been changed in the SmartThings GUI (and/or IDE?).\n **/\ndef updated() {\n    if (\"true\" == configDebugMode) log.trace \"${device.displayName}: updated()\"\n\n    if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {\n        state.updatedLastRanAt = now()\n\n        // Make sure installation has completed:\n        if (!state.isInstalled) { installed() }\n\n        state.debug = (\"true\" == configDebugMode)\n\n        // Convert channel mappings to a map:\n        def cMapping = []\n        cMapping[1] = configCh1Mapping\n        cMapping[2] = configCh2Mapping\n        cMapping[3] = configCh3Mapping\n        cMapping[4] = configCh4Mapping\n        state.channelMapping = cMapping\n\n        // Convert channel thresholds to a map:\n        def cThresholds = []\n        cThresholds[1] = configCh1Threshold.toInteger()\n        cThresholds[2] = configCh2Threshold.toInteger()\n        cThresholds[3] = configCh3Threshold.toInteger()\n        cThresholds[4] = configCh4Threshold.toInteger()\n        state.channelThresholds = cThresholds\n\n        // Convert channel modes to a map:\n        def cModes = []\n        cModes[1] = configParam14_1.toInteger()\n        cModes[2] = configParam14_2.toInteger()\n        cModes[3] = configParam14_3.toInteger()\n        cModes[4] = configParam14_4.toInteger()\n        state.channelModes = cModes\n\n        // Validate Paramter #14 settings:\n        state.isRGBW = ( state.channelModes[1] < 8 ) || ( state.channelModes[2] < 8 ) || ( state.channelModes[3] < 8 ) || ( state.channelModes[4] < 8 )\n        state.isIN   = ( state.channelModes[1] == 8 ) || ( state.channelModes[2] == 8 ) || ( state.channelModes[3] == 8 ) || ( state.channelModes[4] == 8 )\n        state.isOUT  = ( state.channelModes[1] > 8 ) || ( state.channelModes[2] > 8 ) || ( state.channelModes[3] > 8 ) || ( state.channelModes[4] > 8 )\n        if ( state.isRGBW & ( (state.channelModes[1] != state.channelModes[2]) || (state.channelModes[1] != state.channelModes[3]) || (state.channelModes[1] != state.channelModes[4]) ) ) {\n            log.warn \"${device.displayName}: updated(): Invalid combination of RGBW channels detected. All RGBW channels should be identical. You may get weird behaviour!\"\n        }\n        if ( state.isRGBW & ( state.isIN || state.isOUT ) ) log.warn \"${device.displayName}: updated(): Invalid combination of RGBW and IN/OUT channels detected. You may get weird behaviour!\"\n\n        // Call configure() and refresh():\n        return response( [ configure() + refresh() ])\n    }\n    else {\n        log.debug \"updated(): Ran within last 2 seconds so aborting.\"\n    }\n}\n\n/**\n *  configure() - Configure physical device parameters.\n *\n *  Uses values from device preferences.\n **/\ndef configure() {\n    if (state.debug) log.trace \"${device.displayName}: configure()\"\n\n    def cmds = []\n\n    // Note: Parameters #10,#14,#39,#44 have size: 2!\n    // can't use scaledConfigurationValue to set parameters with size < 1 as there is a bug in the configurationV1.configurationSet class.\n    //  See: https://community.smartthings.com/t/zwave-configurationv2-configurationreport-dev-question/9771\n    // Instead, must use intToUnsignedByteArray(number,size) to convert to an unsigned byteArray manually.\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 1, size: 1, configurationValue: [configParam01.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 6, size: 1, configurationValue: [configParam06.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 8, size: 1, configurationValue: [configParam08.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 9, size: 1, configurationValue: [configParam09.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 10, size: 2, configurationValue: intToUnsignedByteArray(configParam10.toInteger(),2)).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 11, size: 1, configurationValue: [configParam11.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 12, size: 1, configurationValue: [configParam12.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 13, size: 1, configurationValue: [configParam13.toInteger()]).format()\n    //  Parameter #14 needs to be reconstituted from each 4-bit channel value.\n    def p14A = (configParam14_1.toInteger() * 0x10) + configParam14_2.toInteger()\n    def p14B = (configParam14_3.toInteger() * 0x10) + configParam14_4.toInteger()\n    if (state.debug) log.debug \"${device.displayName}: configure(): Setting Parameter #14 to: [${p14A},${p14B}]\"\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 14, size: 2, configurationValue: [p14A.toInteger(), p14B.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 16, size: 1, configurationValue: [configParam16.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 30, size: 1, configurationValue: [configParam30.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 38, size: 1, configurationValue: [configParam38.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 39, size: 2, configurationValue: intToUnsignedByteArray(configParam39.toInteger(),2)).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 42, size: 1, configurationValue: [configParam42.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 43, size: 1, configurationValue: [configParam43.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 44, size: 2, configurationValue: intToUnsignedByteArray(configParam44.toInteger(),2)).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 45, size: 1, configurationValue: [configParam45.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 71, size: 1, configurationValue: [configParam71.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 72, size: 1, configurationValue: [configParam72.toInteger()]).format()\n    cmds << zwave.configurationV2.configurationSet(parameterNumber: 73, size: 1, configurationValue: [configParam73.toInteger()]).format()\n\n    // Association Groups:\n    cmds << zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId: []).format()\n    cmds << zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: parseAssocGroup(configAssocGroup01,5)).format()\n    cmds << zwave.associationV2.associationRemove(groupingIdentifier: 2, nodeId: []).format()\n    cmds << zwave.associationV2.associationSet(groupingIdentifier: 2, nodeId: parseAssocGroup(configAssocGroup02,5)).format()\n    cmds << zwave.associationV2.associationRemove(groupingIdentifier: 3, nodeId: []).format()\n    cmds << zwave.associationV2.associationSet(groupingIdentifier: 3, nodeId: parseAssocGroup(configAssocGroup03,5)).format()\n    cmds << zwave.associationV2.associationRemove(groupingIdentifier: 4, nodeId: []).format()\n    cmds << zwave.associationV2.associationSet(groupingIdentifier: 4, nodeId: parseAssocGroup(configAssocGroup04,5)).format()\n    cmds << zwave.associationV2.associationRemove(groupingIdentifier: 5, nodeId: []).format()\n    cmds << zwave.associationV2.associationSet(groupingIdentifier: 5, nodeId: [zwaveHubNodeId]).format() // Add the SmartThings hub (controller) to Association Group #5.\n\n    log.warn \"${device.displayName}: configure(): Device Parameters are being updated. It is recommended to power-cycle the Fibaro device once completed.\"\n\n    return delayBetween(cmds, 500) + getConfigReport()\n}\n\n\n/**********************************************************************\n *  Capability-related Commands:\n **********************************************************************/\n\n /**\n  *  on() - Turn the switch on. [Switch Capability]\n  *\n  *  Only sends commands to RGBW/OUT channels to avoid altering the levels of INPUT channels.\n  **/\n def on() {\n     log.info \"${device.displayName}: on()\"\n\n     def cmds = []\n     def newLevel = 0\n     def isAnyOn = false\n\n     (1..4).each { channel ->\n         // If there is a saved level which is not zero, then apply the saved level:\n         newLevel = device.latestValue(\"savedLevelCh${channel}\") ?: -1\n         if (newLevel.toInteger() > 0) {\n             cmds << setLevelChX(newLevel.toInteger(), channel)\n             isAnyOn = true\n         }\n     }\n\n     if (!isAnyOn) { // However, if none of the channels were turned on, turn them all on.\n         (1..4).each { channel ->\n             if ( 8 != state.channelModes[channel] ) { cmds << onChX(channel)}\n         }\n     }\n\n     return cmds\n }\n\n/**\n *  off() - Turn the switch off. [Switch Capability]\n *\n *  Only sends commands to RGBW/OUT channels to avoid altering the levels of INPUT channels.\n **/\ndef off() {\n    log.info \"${device.displayName}: off()\"\n\n    def cmds = []\n    (1..4).each { i ->\n        if ( 8 != state.channelModes[i] ) { cmds << offChX(i)}\n    }\n    return cmds\n}\n\n/**\n *  setLevel(level, rate) - Set the (aggregate) level. [Switch Level Capability]\n *\n *  Note: rate is ignored as it is not supported.\n *\n *  Calculation of new channel levels is controlled by configLevelSetMode (see preferences).\n *  Only sends commands to RGBW/OUT channels to avoid altering the levels of INPUT channels.\n **/\ndef setLevel(level, rate = 1) {\n    if (state.debug) log.trace \"${device.displayName}: setLevel(): Level: ${level}\"\n    if (level > 100) level = 100\n    if (level < 0) level = 0\n\n    def cmds = []\n\n    if ( \"SCALE\" == configLevelSetMode ) { // SCALE Mode:\n        float currentMaxOutLevel = 0.0\n        (1..4).each { i ->\n            if ( 8 != state.channelModes[i] ) { currentMaxOutLevel = Math.max(currentMaxOutLevel,device.latestValue(\"levelCh${i}\").toInteger()) }\n        }\n\n        if (0.0 == currentMaxOutLevel) { // All OUT levels are currently zero, so just set all to the new level:\n            (1..4).each { i ->\n                if ( 8 != state.channelModes[i] ) { cmds << setLevelChX(level.toInteger(),i) }\n            }\n        }\n        else { // Scale the individual channel levels:\n            float s = level / currentMaxOutLevel\n            (1..4).each { i ->\n                if ( 8 != state.channelModes[i] ) { cmds << setLevelChX( (device.latestValue(\"levelCh${i}\") * s).toInteger(),i) }\n            }\n        }\n    }\n    else { // SIMPLE Mode:\n        (1..4).each { i ->\n            if ( 8 != state.channelModes[i] ) { cmds << setLevelChX(level.toInteger(),i) }\n        }\n    }\n\n    return cmds\n}\n\n/**\n *  setColor() - Set the color. [Color Control Capability]\n *\n *  Accepts a colorMap with the following key combinations (in order of precedence):\n *   red, green, blue, white\n *   red, green, blue\n *   hex\n *   name\n *   hue, saturation, level\n *   red|green|blue|white      [Will only set values that are specified]\n *   hue|saturation|level      [Will use the device's current value for any missing values]\n *\n *  Obeys the channel color mappings defined in the device's preferences.\n *  If a color channel does not exist it is simply ignored.\n **/\ndef setColor(Map colorMap) {\n    if (state.debug) log.trace \"${device.displayName}: setColor(): colorMap: ${colorMap}\"\n\n    def cmds = []\n    def rgbw = []\n\n    if (colorMap.containsKey(\"red\") & colorMap.containsKey(\"green\") & colorMap.containsKey(\"blue\") & colorMap.containsKey(\"white\")) {\n        if (state.debug) log.debug \"${device.displayName}: setColor(): Setting color using RGBW values.\"\n        rgbw = colorMap\n    }\n    if (colorMap.containsKey(\"red\") & colorMap.containsKey(\"green\") & colorMap.containsKey(\"blue\")) {\n        if (state.debug) log.debug \"${device.displayName}: setColor(): Setting color using RGB values.\"\n        rgbw = rgbToRGBW(colorMap)\n    }\n    else if (colorMap.containsKey(\"hex\")) {\n        if (state.debug) log.debug \"${device.displayName}: setColor(): Setting color using hex value.\"\n        rgbw = hexToRGBW(colorMap)\n    }\n    else if (colorMap.containsKey(\"name\")) {\n        if (state.debug) log.debug \"${device.displayName}: setColor(): Setting color using name.\"\n        rgbw = nameToRGBW(colorMap)\n    }\n    else if (colorMap.containsKey(\"hue\") & colorMap.containsKey(\"saturation\") & colorMap.containsKey(\"level\")) {\n        if (state.debug) log.debug \"${device.displayName}: setColor(): Setting color using HSV values.\"\n        rgbw = hsvToRGBW(colorMap)\n    }\n    else if (colorMap.containsKey(\"red\") || colorMap.containsKey(\"green\") || colorMap.containsKey(\"blue\") || colorMap.containsKey(\"white\")) {\n        if (state.debug) log.debug \"${device.displayName}: setColor(): Setting color using partial RGBW values.\"\n        rgbw = colorMap // Don't add any key/values, only those that exist will be set below.\n    }\n    else if (colorMap.containsKey(\"hue\") || colorMap.containsKey(\"saturation\") || colorMap.containsKey(\"level\")) {\n        if (state.debug) log.debug \"${device.displayName}: setColor(): Setting color using partial HSV values.\"\n        def h = (colorMap.containsKey(\"hue\")) ? colorMap.hue : device.latestValue(\"hue\").toInteger()\n        def s = (colorMap.containsKey(\"saturation\")) ? colorMap.saturation : device.latestValue(\"saturation\").toInteger()\n        def l = (colorMap.containsKey(\"level\")) ? colorMap.level : device.latestValue(\"level\").toInteger()\n        rgbw = hsvToRGBW([hue: h, saturation: s, level: l])\n    }\n    else {\n        log.error \"${device.displayName}: setColor(): Cannot obtain color information from colorMap: ${colorMap}\"\n    }\n\n    if (rgbw) {\n        // Apply channel mappings before sending switchColorSet command:\n        def chIndex = [ null, red, green, blue, warmWhite] // These are names of the channels used in switchColorSet.\n        def rgbwMapped = [:]\n        (1..4).each { i ->\n            if ( \"Red\" == state.channelMapping[i] & rgbw.containsKey(\"red\") ) { rgbwMapped << [(chIndex[i]) : rgbw.red] }\n            else if ( \"Green\" == state.channelMapping[i] & rgbw.containsKey(\"green\") ) { rgbwMapped << [(chIndex[i]) : rgbw.green] }\n            else if ( \"Blue\" == state.channelMapping[i] & rgbw.containsKey(\"blue\") ) { rgbwMapped << [(chIndex[i]) : rgbw.blue] }\n            else if ( \"White\" == state.channelMapping[i] & rgbw.containsKey(\"white\") ) { rgbwMapped << [(chIndex[i]) : rgbw.white] }\n\n            sendEvent(name: \"savedLevelCh${i}\", value: null) // Wipe savedLevel.\n        }\n        cmds << zwave.switchColorV3.switchColorSet(rgbwMapped).format()\n\n        // Alternatively, could use switchMultilevelSet commands via setLevel* (but switchColorSet is more efficient):\n        //cmds << setLevelRed(Math.round(rgbw.red * 99/255)) // setLevel* uses 99 as max.\n        //cmds << setLevelGreen(Math.round(rgbw.green * 99/255))\n        //cmds << setLevelBlue(Math.round(rgbw.blue * 99/255))\n        //cmds << setLevelWhite(Math.round(rgbw.white * 99/255))\n\n        sendEvent(name: \"activeProgram\", value: 0) // Wipe activeProgram.\n\n        delayBetween(cmds,200)\n    }\n}\n\n/**\n *  setHue(percent) - Set the color hue. [Color Control Capability]\n **/\ndef setHue(percent) {\n    if (state.debug) log.trace \"${device.displayName}: setHue(): Hue: ${percent}\"\n    setColor([hue: percent])\n}\n\n/**\n *  setSaturation(percent) - Set the color saturation. [Color Control Capability]\n **/\ndef setSaturation(percent) {\n    if (state.debug) log.trace \"${device.displayName}: setSaturation(): Saturation: ${percent}\"\n    setColor([saturation: percent])\n}\n\n/**\n *  poll() - Polls the device. [Polling Capability]\n *\n *  The SmartThings platform seems to poll devices randomly every 6-8mins.\n **/\ndef poll() {\n    if (state.debug) log.trace \"${device.displayName}: poll()\"\n    refresh()\n}\n\n/**\n *  refresh() - Refreshes values from the physical device. [Refresh Capability]\n **/\ndef refresh() {\n    if (state.debug) log.trace \"${device.displayName}: refresh()\"\n    def cmds = []\n\n    if (state.isIN) { // There are INPUT channels, so we must get channel levels using switchMultilevelGet:\n        (2..5).each { cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: it).encapsulate(zwave.switchMultilevelV2.switchMultilevelGet()).format()) }\n    }\n    else { // There are no INPUT channels, so we can use switchColorGet for greater accuracy:\n        (0..4).each { cmds << response(zwave.switchColorV3.switchColorGet(colorComponentId: it).format()) }\n    }\n\n    cmds << response(zwave.meterV3.meterGet(scale: 0).format()) // Get energy MeterReport\n    cmds << response(zwave.meterV3.meterGet(scale: 2).format()) // Get power MeterReport\n    delayBetween(cmds,200)\n}\n\n\n/**********************************************************************\n *  Custom Commands:\n **********************************************************************/\n\n/**\n *  reset() - Reset Accumulated Energy.\n **/\ndef reset() {\n    if (state.debug) log.trace \"${device.displayName}: reset()\"\n\n    state.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"lastReset\", value: state.lastReset)\n\n    return [\n        zwave.meterV3.meterReset().format(),\n        zwave.meterV3.meterGet(scale: 0).format()\n    ]\n}\n\n/**\n *  on*() - Set switch for an individual channel to \"on\".\n *\n *  These commands all map to onChX().\n **/\ndef onCh1() { onChX(1) }\ndef onCh2() { onChX(2) }\ndef onCh3() { onChX(3) }\ndef onCh4() { onChX(4) }\ndef onRed() {\n    def cmds = []\n    (1..4).each { i -> if ( \"Red\" == state.channelMapping[i] ) { cmds << onChX(i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: onRed(): There are no channels mapped to Red!\"\n    return cmds\n}\ndef onGreen() {\n    def cmds = []\n    (1..4).each { i -> if ( \"Green\" == state.channelMapping[i] ) { cmds << onChX(i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: onGreen(): There are no channels mapped to Green!\"\n    return cmds\n}\ndef onBlue() {\n    def cmds = []\n    (1..4).each { i -> if ( \"Blue\" == state.channelMapping[i] ) { cmds << onChX(i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: onBlue(): There are no channels mapped to Blue!\"\n    return cmds\n}\ndef onWhite() {\n    def cmds = []\n    (1..4).each { i -> if ( \"White\" == state.channelMapping[i] ) { cmds << onChX(i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: onWhite(): There are no channels mapped to White!\"\n    return cmds\n}\n\n/**\n *  off*() - Set switch for an individual channel to \"off\".\n *\n *  These commands all map to offChX().\n **/\ndef offCh1() { offChX(1) }\ndef offCh2() { offChX(2) }\ndef offCh3() { offChX(3) }\ndef offCh4() { offChX(4) }\ndef offRed() {\n    def cmds = []\n    (1..4).each { i -> if ( \"Red\" == state.channelMapping[i] ) { cmds << offChX(i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: offRed(): There are no channels mapped to Red!\"\n    return cmds\n}\ndef offGreen() {\n    def cmds = []\n    (1..4).each { i -> if ( \"Green\" == state.channelMapping[i] ) { cmds << offChX(i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: offGreen(): There are no channels mapped to Green!\"\n    return cmds\n}\ndef offBlue() {\n    def cmds = []\n    (1..4).each { i -> if ( \"Blue\" == state.channelMapping[i] ) { cmds << offChX(i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: offBlue(): There are no channels mapped to Blue!\"\n    return cmds\n}\ndef offWhite() {\n    def cmds = []\n    (1..4).each { i -> if ( \"White\" == state.channelMapping[i] ) { cmds << offChX(i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: offWhite(): There are no channels mapped to White!\"\n    return cmds\n}\n\n/**\n *  setLevel*() - Set level of an individual channel.\n *\n *  These commands all map to setLevelChX().\n **/\ndef setLevelCh1(level) { setLevelChX(level, 1) }\ndef setLevelCh2(level) { setLevelChX(level, 2) }\ndef setLevelCh3(level) { setLevelChX(level, 3) }\ndef setLevelCh4(level) { setLevelChX(level, 4) }\ndef setLevelRed(level) {\n    def cmds = []\n    (1..4).each { i -> if ( \"Red\" == state.channelMapping[i] ) { cmds << setLevelChX(level,i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: setLevelRed(): There are no channels mapped to Red!\"\n    return cmds\n}\ndef setLevelGreen(level) {\n    def cmds = []\n    (1..4).each { i -> if ( \"Green\" == state.channelMapping[i] ) { cmds << setLevelChX(level,i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: setLevelGreen(): There are no channels mapped to Green!\"\n    return cmds\n}\ndef setLevelBlue(level) {\n    def cmds = []\n    (1..4).each { i -> if ( \"Blue\" == state.channelMapping[i] ) { cmds << setLevelChX(level,i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: setLevelBlue(): There are no channels mapped to Blue!\"\n    return cmds\n}\ndef setLevelWhite(level) {\n    def cmds = []\n    (1..4).each { i -> if ( \"White\" == state.channelMapping[i] ) { cmds << setLevelChX(level,i) } }\n    if (cmds.empty) log.warn \"${device.displayName}: setLevelWhite(): There are no channels mapped to White!\"\n    return cmds\n}\n\n/**\n *  *color*() - Set a colour by name.\n *\n *  These commands all map to setColor().\n **/\ndef black()     { setColor(name: \"black\") }\ndef white()     { setColor(name: \"white\") }\ndef red()       { setColor(name: \"red\") }\ndef green()     { setColor(name: \"green\") }\ndef blue()      { setColor(name: \"blue\") }\ndef cyan()      { setColor(name: \"cyan\") }\ndef magenta()   { setColor(name: \"magenta\") }\ndef orange()    { setColor(name: \"orange\") }\ndef purple()    { setColor(name: \"purple\") }\ndef yellow()    { setColor(name: \"yellow\") }\ndef pink()      { setColor(name: \"pink\") }\ndef coldWhite() { setColor(name: \"coldWhite\") }\ndef warmWhite() { setColor(name: \"warmWhite\") }\n\n/**\n *  startProgram(programNumber) - Start a built-in animation program.\n **/\ndef startProgram(programNumber) {\n    if (state.debug) log.trace \"${device.displayName}: startProgram(): programNumber: ${programNumber}\"\n\n    if (state.isIN | state.isOUT) {\n        log.warn \"${device.displayName}: Built-in programs work with RGBW channels only, they will not function when using IN/OUT channels!\"\n    }\n    else if (programNumber > 0 & programNumber <= 10) {\n        (1..4).each { sendEvent(name: \"savedLevelCh${it}\", value: device.latestValue(\"levelCh${it}\").toInteger(), displayed: false) } // Save levels for all channels.\n        sendEvent(name: \"activeProgram\", value: programNumber, displayed: false)\n        sendEvent(name: \"colorName\", value: \"program\")\n        return zwave.configurationV1.configurationSet(configurationValue: [programNumber], parameterNumber: 72, size: 1).format()\n    }\n    else {\n        log.warn \"${device.displayName}: startProgram(): Invalid programNumber: ${programNumber}\"\n    }\n}\n\n/**\n *  start*() - Start built-in animation program by name.\n **/\ndef startFireplace() { startProgram(6) }\ndef startStorm()     { startProgram(7) }\ndef startDeepFade()  { startProgram(8) }\ndef startLiteFade()  { startProgram(9) }\ndef startPolice()    { startProgram(10) }\n\n/**\n *  stopProgram() - Stop animation program (if running).\n **/\ndef stopProgram() {\n    if (state.debug) log.trace \"${device.displayName}: startProgram()\"\n\n    sendEvent(name: \"activeProgram\", value: 0, displayed: false)\n    return on() // on() will automatically restore levels.\n}\n\n\n/**********************************************************************\n *  Private Helper Methods:\n **********************************************************************/\n\n/**\n *  getSupportedCommands() - Returns a map of the command versions supported by the device.\n *\n *  Used by parse(), and to extract encapsulated commands from MultiChannelCmdEncap,\n *   MultiInstanceCmdEncap, SecurityMessageEncapsulation, and Crc16Encap messages.\n *\n *  The Fibaro RGBW Controller supports the following commmand classes:\n *   All Switch (0x27) : V1\n *   Association (0x85) : V2\n *   Basic (0x20) : V1\n *   Color Control (0x33) : V3\n *   Configuration (0x70) : V2\n *   Firmware Update Meta Data (0x7A) : V2\n *   Manufacturer Specific (0x72) : V2\n *   Meter (0x32) : V3\n *   Multi Channel (0x60) : V3\n *   Multilevel Sensor (0x31) : V2\n *   Switch Multilevel (0x26) : V2\n *   Version (0x86) : V1\n *\n **/\nprivate getSupportedCommands() {\n    return [0x20: 1, 0x26: 2, 0x27: 1, 0x31:2, 0x32: 3, 0x33: 3, 0x60: 3, 0x70: 2, 0x72: 2, 0x85: 2, 0x86: 1, 0x7A: 2]\n}\n\n/**\n *  byteArrayToInt(byteArray)\n *\n *  Converts an unsigned byte array to a int.\n *  Should use ByteBuffer, but it's not available in SmartThings.\n **/\nprivate byteArrayToInt(byteArray) {\n    // return java.nio.ByteBuffer.wrap(byteArray as byte[]).getInt()\n    def i = 0\n    byteArray.reverse().eachWithIndex { b, ix -> i += b * (0x100 ** ix) }\n    return i\n}\n\n/**\n *  intToUnsignedByteArray(number, size)\n *\n *  Converts an unsigned int to an unsigned byte array of set size.\n **/\nprivate intToUnsignedByteArray(number, size)  {\n    if (number < 0) {\n        log.error \"${device.displayName}: intToUnsignedByteArray(): Doesn't work with negative number: ${number}\"\n    }\n    else {\n        def uBA = new BigInteger(number).toByteArray() // This returns a SIGNED byte array.\n        uBA = uBA.collect { (it < 0) ? it & 0xFF : it } // Convert from signed to unsigned.\n        while (uBA.size() > size) { uBA = uBA.drop(1) } // Trim leading bytes if too long. (takeRight() is not available)\n        while (uBA.size() < size) { uBA = [0] + uBA } // Pad with leading zeros if too short.\n        return uBA\n    }\n}\n\n/**\n * parseAssocGroup(string, maxNodes)\n *\n *  Converts a comma-delimited string into a list of integers.\n *  Checks that all elements are integer numbers, and removes any that are not.\n *  Checks that the final list contains no more than maxNodes.\n */\nprivate parseAssocGroup(string, maxNodes) {\n    if (state.debug) log.trace \"${device.displayName}: parseAssocGroup(): Translating string: ${string}\"\n\n    if (string) {\n        def nodeList = string.split(',')\n        nodeList = nodeList.collect { node ->\n            if (node.isInteger()) { node.toInteger() }\n            else { log.warn \"${device.displayName}: parseAssocGroup(): Cannot parse: ${node}\"}\n        }\n        nodeList = nodeList.findAll() // findAll() removes the nulls.\n        if (nodeList.size() > maxNodes) { log.warn \"${device.displayName}: parseAssocGroup(): Number of nodes is greater than ${maxNodes}!\" }\n        return nodeList.take(maxNodes)\n    }\n    else {\n        return []\n    }\n}\n\n/**\n *  zwaveEndPointEvent(sourceEndPoint, value)\n *\n *   Int        sourceEndPoint      ID of endPoint. 1 = Aggregate, 2 = Ch1, 3 = Ch2...\n *   Short      value               Expected range [0..255].\n *\n *  This method handles level reports received via several different command classes (BasicReport,\n *  SwitchMultilevelReport, SwitchColorReport).\n *\n *  switch and level attributes for the physical channel are updated (e.g. switchCh1, levelCh1).\n *\n *  If the channel is mapped to a colour, the colour's switch and level attributes are also updated\n *  (e.g. switchBlue, levelBlue).\n *\n *  Aggregate device atributes (switch, level, hue, saturation, color, colorName) are also updated.\n **/\nprivate zwaveEndPointEvent(sourceEndPoint, value) {\n    if (state.debug) log.trace \"${device.displayName}: zwaveEndPointEvent(): EndPoint ${sourceEndPoint} has value: ${value}\"\n\n    def channel = sourceEndPoint - 1\n    def mapping = state.channelMapping[channel]\n    def isColor = ( mapping in [\"Red\", \"Green\", \"Blue\", \"White\"] )\n    def percent = Math.round (value * 100 / 255)\n\n    if ( 1 == sourceEndPoint ) { // EndPoint 1 is the aggregate channel, which is calculated later. IGNORE.\n        if (state.debug) log.debug \"${device.displayName}: zwaveEndPointEvent(): MultiChannelCmdEncap from endpoint 1 ignored.\"\n    }\n    else if ( (sourceEndPoint > 1) & (sourceEndPoint < 6) ) { // Physical channel #1..4\n\n        // Update level:\n        log.info \"${device.displayName}: Channel ${channel} level is ${percent}%.\"\n        sendEvent(name: \"levelCh${channel}\", value: percent, unit: \"%\")\n        if (isColor) sendEvent(name: \"level${mapping}\", value: percent, unit: \"%\")\n\n        // Update switch:\n        if ( percent >= state.channelThresholds[channel].toInteger() ) {\n            log.info \"${device.displayName}: Channel ${channel} is on.\"\n            sendEvent(name: \"switchCh${channel}\", value: \"on\")\n            if (isColor) sendEvent(name: \"switch${mapping}\", value: \"on\")\n        } else {\n            log.info \"${device.displayName}: Channel ${channel} is off.\"\n            sendEvent(name: \"switchCh${channel}\", value: \"off\")\n            if (isColor) sendEvent(name: \"switch${mapping}\", value: \"off\")\n        }\n\n        // If channel maps to a color, update hue, saturation, and color attributes:\n        if (isColor) {\n            def colorMap\n            switch (mapping) {\n                case \"Red\":\n                    colorMap = [ red: value,\n                               green: Math.round(device.latestValue(\"levelGreen\").toInteger() * 255/100),\n                                blue: Math.round(device.latestValue(\"levelBlue\").toInteger() * 255/100),\n                               white: Math.round(device.latestValue(\"levelWhite\").toInteger() * 255/100)]\n                    break\n                case \"Green\":\n                    colorMap = [ red: Math.round(device.latestValue(\"levelRed\").toInteger() * 255/100),\n                               green: value,\n                                blue: Math.round(device.latestValue(\"levelBlue\").toInteger() * 255/100),\n                               white: Math.round(device.latestValue(\"levelWhite\").toInteger() * 255/100)]\n                    break\n                case \"Blue\":\n                    colorMap = [ red: Math.round(device.latestValue(\"levelRed\").toInteger() * 255/100),\n                               green: Math.round(device.latestValue(\"levelGreen\").toInteger() * 255/100),\n                                blue: value,\n                               white: Math.round(device.latestValue(\"levelWhite\").toInteger() * 255/100)]\n                    break\n                case \"White\":\n                    colorMap = [ red: Math.round(device.latestValue(\"levelRed\").toInteger() * 255/100),\n                               green: Math.round(device.latestValue(\"levelGreen\").toInteger() * 255/100),\n                                blue: Math.round(device.latestValue(\"levelBlue\").toInteger() * 255/100),\n                               white: value]\n                    break\n                default:\n                    colorMap = [ red: Math.round(device.latestValue(\"levelRed\").toInteger() * 255/100),\n                               green: Math.round(device.latestValue(\"levelGreen\").toInteger() * 255/100),\n                                blue: Math.round(device.latestValue(\"levelBlue\").toInteger() * 255/100),\n                               white: Math.round(device.latestValue(\"levelWhite\").toInteger() * 255/100)]\n                    break\n            }\n            colorMap << rgbwToHSV(colorMap) // Add HSV values into the colorMap.\n            colorMap << rgbwToHex(colorMap) // Add hex into the colorMap.\n            colorMap << rgbwToName(colorMap) // Add name into the colorMap.\n\n            sendEvent(name: \"hue\", value: colorMap.hue, unit: \"%\")\n            sendEvent(name: \"saturation\", value: colorMap.saturation, unit: \"%\")\n            sendEvent(name: \"colorName\", value: \"${colorMap.name}\")\n            sendEvent(name: \"color\", value: \"${colorMap}\", displayed: false)\n\n            log.info \"${device.displayName}: Color updated: ${colorMap}\"\n        }\n    }\n    else {\n        log.warn \"${device.displayName}: SwitchMultilevelReport recieved from unknown endpoint: ${sourceEndPoint}\"\n    }\n\n    // Calculate aggregate switch attribute:\n    // TODO: Add shortcuts here to check if the channel we are processing is IN or OUT.\n    def newSwitch = \"off\"\n    if ( \"IN\" == configAggregateSwitchMode) { // Build aggregate only from INput channels.\n        (1..4).each { i ->\n            if (( 8 == state.channelModes[i] ) & ( \"on\" == device.latestValue(\"switchCh${i}\"))) { newSwitch = \"on\" }\n        }\n    } else if (\"OUT\" == configAggregateSwitchMode) { // Build aggregate only from RGBW/OUT channels.\n        (1..4).each { i ->\n            if (( 8 != state.channelModes[i] ) & ( \"on\" == device.latestValue(\"switchCh${i}\"))) { newSwitch = \"on\" }\n        }\n    } else { // Build aggregate from ALL channels.\n        (1..4).each { i ->\n            if ( \"on\" == device.latestValue(\"switchCh${i}\")) { newSwitch = \"on\" }\n        }\n    }\n    log.info \"${device.displayName}: Switch is ${newSwitch}.\"\n    sendEvent(name: \"switch\", value: newSwitch)\n\n    // Calculate aggregate level attribute:\n    def newLevel = 0\n    if ( \"IN\" == configAggregateSwitchMode) { // Build aggregate only from INput channels.\n        (1..4).each { i ->\n            if ( 8 == state.channelModes[i] ) { newLevel = Math.max(newLevel,device.latestValue(\"levelCh${i}\").toInteger()) }\n        }\n    } else if (\"OUT\" == configAggregateSwitchMode) { // Build aggregate only from RGBW/OUT channels.\n        (1..4).each { i ->\n            if ( 8 != state.channelModes[i] ) { newLevel = Math.max(newLevel,device.latestValue(\"levelCh${i}\").toInteger()) }\n        }\n    } else { // Build aggregate from ALL channels.\n        (1..4).each { i ->\n            newLevel = Math.max(newLevel,device.latestValue(\"levelCh${i}\").toInteger())\n        }\n    }\n    log.info \"${device.displayName}: Level is ${newLevel}.\"\n    sendEvent(name: \"level\", value: newLevel, unit: \"%\")\n\n    // Should send the result of a CreateEvent...\n    return \"Processed channel level\"\n}\n\n/**\n *  onChX() - Set switch for an individual channel to \"on\".\n *\n *  If channel is RGBW/OUT, restore the saved level (if there is one, else 100%).\n *  If channel is an INPUT channel, don't issue command. Log warning instead.\n **/\nprivate onChX(channel) {\n    log.info \"${device.displayName}: onX(): Setting channel ${channel} switch to on.\"\n\n    def cmds = []\n    if (channel < 1 || channel > 4 ) {\n        log.warn \"${device.displayName}: onX(): Channel ${channel} does not exist!\"\n    }\n    else if ( 8 == state.channelModes[channel] ) {\n        log.warn \"${device.displayName}: onX(): Channel ${channel} is an INPUT channel. Command not sent.\"\n        cmds << zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: (channel + 1) ).encapsulate(zwave.switchMultilevelV2.switchMultilevelGet()).format() // Endpoint = channel + 1\n    }\n    else {\n        def newLevel =  device.latestValue(\"savedLevelCh${channel}\") ?: 100\n        newLevel =  ( 0 == newLevel.toInteger() ) ? 99 : Math.round(newLevel.toInteger() * 99 / 100 ) // scale level for switchMultilevelSet.\n        cmds << zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: (channel + 1) ).encapsulate(zwave.switchMultilevelV2.switchMultilevelSet(value: newLevel.toInteger())).format() // Endpoint = channel + 1\n        sendEvent(name: \"savedLevelCh${channel}\", value: null) // Wipe savedLevel.\n        sendEvent(name: \"activeProgram\", value: 0) // Wipe activeProgram.\n    }\n\n    return cmds\n}\n\n/**\n *  offChX() - Set switch for an individual channel to \"off\".\n *\n *  If channel is RGBW/OUT, save the level and turn off.\n *  If channel is an INPUT channel, don't issue command. Log warning instead.\n **/\nprivate offChX(channel) {\n    log.info \"${device.displayName}: offX(): Setting channel ${channel} switch to off.\"\n\n    def cmds = []\n    if (channel > 4 || channel < 1 ) {\n        log.warn \"${device.displayName}: offX(): Channel ${channel} does not exist!\"\n    }\n    else if ( 8 == state.channelModes[channel] ) {\n        log.warn \"${device.displayName}: offX(): Channel ${channel} is an INPUT channel. Command not sent.\"\n        cmds << zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: (channel + 1) ).encapsulate(zwave.switchMultilevelV2.switchMultilevelGet()).format() // endPoint = channel + 1\n    }\n    else {\n        sendEvent(name: \"savedLevelCh${channel}\", value: device.latestValue(\"levelCh${channel}\").toInteger()) // Save level to 'hidden' attribute.\n        sendEvent(name: \"activeProgram\", value: 0) // Wipe activeProgram.\n        cmds << zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: (channel + 1) ).encapsulate(zwave.switchMultilevelV2.switchMultilevelSet(value: 0)).format() // endPoint = channel + 1\n    }\n\n    return cmds\n}\n\n/**\n *  setLevelChX() - Set level of an individual channel.\n *\n *  If channel is an INPUT channel, don't issue command. Log warning instead.\n *\n *  The Fibaro RGBW Controller does not support dimmingDuration. Instead,\n *  dimming durations are configured using device parameters (8/9/10/11).\n *\n **/\nprivate setLevelChX(level, channel) {\n    log.info \"${device.displayName}: setLevelChX(): Setting channel ${channel} to level: ${level}.\"\n\n    def cmds = []\n    if (channel > 4 || channel < 1 ) {\n        log.warn \"${device.displayName}: setLevelChX(): Channel ${channel} does not exist!\"\n    }\n    else if ( 8 == state.channelModes[channel] ) {\n        log.warn \"${device.displayName}: setLevelChX(): Channel ${channel} is an INPUT channel. Command not sent.\"\n    }\n    else {\n        if (level < 0) level = 0\n        if (level > 100) level = 100\n        level = Math.round(level * 99 / 100 ) // scale level for switchMultilevelSet.\n        cmds << zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: (channel + 1) ).encapsulate(zwave.switchMultilevelV2.switchMultilevelSet(value: level.toInteger())).format() // Endpoint = channel + 1\n        sendEvent(name: \"savedLevelCh${channel}\", value: null) // Wipe savedLevel.\n        sendEvent(name: \"activeProgram\", value: 0) // Wipe activeProgram.\n    }\n\n    return cmds\n}\n\n/**\n *  rgbToRGBW(colorMap)\n *\n *  Adds white key to a colorMap containing red, green, and blue keys.\n *  For now, the white value is calculated as min(red,green,blue).\n *\n *  A more-complicated translation is discussed here:\n *   http://stackoverflow.com/questions/21117842/converting-an-rgbw-color-to-a-standard-rgb-hsb-rappresentation\n *  But for now we're keeping it simple.\n **/\nprivate rgbToRGBW(Map colorMap) {\n    if (state.debug) log.trace \"${device.displayName}: rgbToRGBW(): Translating colorMap: ${colorMap}\"\n\n    if (colorMap.containsKey(\"red\") & colorMap.containsKey(\"green\") & colorMap.containsKey(\"blue\")) {\n        def w = [colorMap.red, colorMap.green, colorMap.blue].min()\n        return colorMap << [ white: w ]\n    }\n    else {\n        log.error \"${device.displayName}: rgbToRGBW(): Cannot obtain color information from colorMap: ${colorMap}\"\n    }\n}\n\n/**\n *  hexToRGBW(colorMap)\n *\n *  Adds red, green, blue, and white keys to a colorMap containing a hex key.\n **/\nprivate hexToRGBW(Map colorMap) {\n    if (state.debug) log.trace \"${device.displayName}: hexToRGBW(): Translating colorMap: ${colorMap}\"\n\n    if (colorMap.containsKey(\"hex\")) {\n        def r = Integer.parseInt(colorMap.hex.substring(1,3),16)\n        def g = Integer.parseInt(colorMap.hex.substring(3,5),16)\n        def b = Integer.parseInt(colorMap.hex.substring(5,7),16)\n        def w = [r, g, b].min()\n        return colorMap << [ red: r, green: g, blue: b, white: w]\n    }\n    else {\n        log.error \"${device.displayName}: hexToRGBW(): Cannot obtain color information from colorMap: ${colorMap}\"\n    }\n}\n\n/**\n *  rgbwToHex(colorMap)\n *\n *  Adds hex key to a colorMap containing red, green, and blue keys.\n *  The white value is just ignored.\n **/\nprivate rgbwToHex(Map colorMap) {\n    if (state.debug) log.trace \"${device.displayName}: rgbwToHex(): Translating colorMap: ${colorMap}\"\n\n    if (colorMap.containsKey(\"red\") & colorMap.containsKey(\"green\") & colorMap.containsKey(\"blue\")) {\n        def r = hex(colorMap.red,2)\n        def g = hex(colorMap.green,2)\n        def b = hex(colorMap.blue,2)\n        return colorMap << [ hex: \"#${r}${g}${b}\" ]\n    }\n    else {\n        log.error \"${device.displayName}: rgbwToHex(): Cannot obtain color information from colorMap: ${colorMap}\"\n    }\n}\n\n/**\n *  hex(value, width=2)\n *\n *  Formats an int as a hex string.\n **/\nprivate hex(value, width=2) {\n    def s = new BigInteger(Math.round(value).toString()).toString(16)\n    while (s.size() < width) { s = \"0\" + s }\n    return s\n}\n\n/**\n *  hsvToRGBW(colorMap)\n *\n *  Adds red, green, blue, and white keys to a colorMap containing hue, saturation, level (value) keys.\n **/\nprivate hsvToRGBW(Map colorMap) {\n    if (state.debug) log.trace \"${device.displayName}: hsvToRGBW(): Translating colorMap: ${colorMap}\"\n\n    if (colorMap.containsKey(\"hue\") & colorMap.containsKey(\"saturation\") & colorMap.containsKey(\"level\")) {\n        float h = colorMap.hue / 100\n        while (h >= 1) h -= 1\n        float s = colorMap.saturation / 100\n        float v = colorMap.level * 255 / 100\n\n        int d = (int) h * 6\n        float f = (h * 6) - d\n        int n = Math.round(v)\n        int p = Math.round(v * (1 - s))\n        int q = Math.round(v * (1 - f * s))\n        int t = Math.round(v * (1 - (1 - f) * s))\n\n        switch (d) {\n          case 0: return colorMap << [ red: n, green: t, blue: p, white: [n,t,p].min() ]\n          case 1: return colorMap << [ red: q, green: n, blue: p, white: [q,n,p].min() ]\n          case 2: return colorMap << [ red: p, green: n, blue: t, white: [p,n,t].min() ]\n          case 3: return colorMap << [ red: p, green: q, blue: n, white: [p,q,n].min() ]\n          case 4: return colorMap << [ red: t, green: p, blue: n, white: [t,p,n].min() ]\n          case 5: return colorMap << [ red: n, green: p, blue: q, white: [n,p,q].min() ]\n        }\n    }\n    else {\n        log.error \"${device.displayName}: hsvToRGBW(): Cannot obtain color information from colorMap: ${colorMap}\"\n    }\n}\n\n/**\n *  rgbwToHSV(colorMap)\n *\n *  Adds hue, saturation, level (value/brightness) keys to a colorMap containing red, green, and blue keys.\n **/\nprivate rgbwToHSV(Map colorMap) {\n    if (state.debug) log.trace \"${device.displayName}: rgbwToHSV(): Translating colorMap: ${colorMap}\"\n\n    if (colorMap.containsKey(\"red\") & colorMap.containsKey(\"green\") & colorMap.containsKey(\"blue\")) { // Don't test for white key.\n\n        float r = colorMap.red / 255f\n        float g = colorMap.green / 255f\n        float b = colorMap.blue / 255f\n        float w = (colorMap.white) ? colorMap.white / 255f : 0.0\n        float max = [r, g, b].max()\n        float min = [r, g, b].min()\n        float delta = max - min\n\n        float h,s,v = 0\n\n        if (delta) {\n            s = delta / max\n            if (r == max) {\n                h = ((g - b) / delta) / 6\n            } else if (g == max) {\n                h = (2 + (b - r) / delta) / 6\n            } else {\n                h = (4 + (r - g) / delta) / 6\n            }\n            while (h < 0) h += 1\n            while (h >= 1) h -= 1\n        }\n\n        v = [max,w].max() // The white value contributes to brightness only.\n\n        return colorMap << [ hue: h * 100, saturation: s * 100, level: Math.round(v * 100) ] // hue and sat are not rounded.\n    }\n    else {\n        log.error \"${device.displayName}: rgbwToHSV(): Cannot obtain color information from colorMap: ${colorMap}\"\n    }\n}\n\n/**\n *  getPresetColors()\n *\n *  Returns a map of preset colors. Used by nameToRGBW() and rgbwToName().\n **/\nprivate getPresetColors() {\n    return [\n        [name: \"black\",     red:   0, green:   0, blue:   0, white:   0 ],\n        [name: \"white\",     red: 255, green: 255, blue: 255, white: 255 ],\n        [name: \"red\",       red: 255, green:   0, blue:   0, white:   0 ],\n        [name: \"green\",     red:   0, green: 255, blue:   0, white:   0 ],\n        [name: \"blue\",      red:   0, green:   0, blue: 255, white:   0 ],\n        [name: \"cyan\",      red:   0, green: 255, blue: 255, white:   0 ],\n        [name: \"magenta\",   red: 255, green:   0, blue:  64, white:   0 ],\n        [name: \"orange\",    red: 255, green: 102, blue:   0, white:   0 ],\n        [name: \"purple\",    red: 170, green:   0, blue: 255, white:   0 ],\n        [name: \"yellow\",    red: 255, green: 160, blue:   0, white:   0 ],\n        [name: \"pink\",      red: 255, green:  50, blue: 204, white:   0 ],\n        [name: \"coldWhite\", red: 255, green: 255, blue: 255, white:   0 ],\n        [name: \"warmWhite\", red:   0, green:   0, blue:   0, white: 255 ]\n    ]\n}\n\n/**\n *  nameToRGBW(colorMap)\n *\n *  Adds red, green, blue, and white keys to a colorMap containing a name key.\n **/\nprivate nameToRGBW(Map colorMap) {\n    if (state.debug) log.trace \"${device.displayName}: nameToRGBW(): Translating colorMap: ${colorMap}\"\n\n    if (colorMap.containsKey(\"name\")) {\n        def rgbwMap = getPresetColors().find { it.name == colorMap.name }\n        if (rgbwMap) {\n            return colorMap << rgbwMap\n        }\n        else {\n            log.error \"${device.displayName}: nameToRGBW(): Cannot translate color name: ${colorMap.name}\"\n        }\n    }\n    else {\n        log.error \"${device.displayName}: nameToRGBW(): Cannot obtain color information from colorMap: ${colorMap}\"\n    }\n}\n\n/**\n *  rgbwToName(colorMap)\n *\n *  Adds a name key to a colorMap containing red, green, blue, white keys.\n *  Allows a tolerance of 10 on each r/g/b channel, and 50 on white channel.\n *  If color cannot be matched to a named preset color, name: \"custom\" is returned.\n **/\nprivate rgbwToName(Map colorMap) {\n    if (state.debug) log.trace \"${device.displayName}: rgbwToName(): Translating colorMap: ${colorMap}\"\n\n    if (colorMap.containsKey(\"red\") & colorMap.containsKey(\"green\") & colorMap.containsKey(\"blue\")) {\n\n        def t = 10\n        def r = colorMap.red\n        def g = colorMap.green\n        def b = colorMap.blue\n        def w = (colorMap.white) ?: 0\n\n        def match = getPresetColors().find { (it.red >= r-t) & (it.red <= r+t) &\n                                             (it.green >= g-t) & (it.green <= g+t) &\n                                             (it.blue >= b-t) & (it.blue <= b+t) &\n                                             (it.white >= w- (5*t)) & (it.white <= w+(5*t))\n                                           }\n\n        if (match) {\n            if (state.debug) log.trace \"${device.displayName}: rgbwToName(): Found match: ${match.name}\"\n            return colorMap << [name: match.name]\n        }\n        else {\n            return colorMap << [name: \"custom\"]\n        }\n    }\n    else {\n        log.error \"${device.displayName}: rgbwToName(): Cannot obtain color information from colorMap: ${colorMap}\"\n    }\n}\n\n\n/**********************************************************************\n *  Testing Commands:\n **********************************************************************/\n\n/**\n * getConfigReport() - Get current device parameters and output to debug log.\n *\n *  The device settings in the UI cannot be updated due to platform restrictions.\n */\ndef getConfigReport() {\n    if (state.debug) log.trace \"${device.displayName}: getConfigReport()\"\n    def cmds = []\n\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 1).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 6).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 8).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 9).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 10).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 11).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 12).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 13).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 14).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 16).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 30).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 38).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 39).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 42).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 43).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 44).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 45).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 71).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 72).format()\n    cmds << zwave.configurationV2.configurationGet(parameterNumber: 73).format()\n\n    // Request Association Reports:\n    cmds << zwave.associationV2.associationGet(groupingIdentifier:1).format()\n    cmds << zwave.associationV2.associationGet(groupingIdentifier:2).format()\n    cmds << zwave.associationV2.associationGet(groupingIdentifier:3).format()\n    cmds << zwave.associationV2.associationGet(groupingIdentifier:4).format()\n    cmds << zwave.associationV2.associationGet(groupingIdentifier:5).format()\n\n    // Request Manufacturer, Version, Firmware Reports:\n    cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()\n    cmds << zwave.versionV1.versionGet().format()\n    cmds << zwave.firmwareUpdateMdV2.firmwareMdGet().format()\n\n    return delayBetween(cmds,800) // Need log delay here, otherwise the IDE Live Logging can't keep up.\n}\n\n/**\n *  test()\n **/\ndef test() {\n    log.trace \"$device.displayName: test()\"\n\n    def cmds = []\n\n    // EXAMPLE COMMANDS:\n\n    // Verify device configuration:\n    //cmds << getConfigReport()\n\n    // Setting Color:\n    //cmds << setColor(red: 255, green: 128, blue: 66)\n    //cmds << setColor(hex: \"#7FFFD4\")\n    //cmds << setColor(name: \"pink\")\n\n    // Programs:\n    //cmds << startProgram(7)\n\n    // Set device paramters:\n    //cmds << response(zwave.configurationV1.configurationSet(configurationValue: [17,17], parameterNumber: 14, size: 2)) // 4xRGB\n    //cmds << response(zwave.configurationV1.configurationSet(configurationValue: [17,24], parameterNumber: 14, size: 2)) // 3xRGB, I4=0-10V.\n    //cmds << response(zwave.configurationV1.configurationSet(configurationValue: [136,136], parameterNumber: 14, size: 2)) // All 0-10v inputs\n    //cmds << response(zwave.configurationV1.configurationSet(configurationValue: [153,152], parameterNumber: 14, size: 2)) // 3x(OUT momentary/Normal), I4=INPUT\n    //cmds << response(zwave.configurationV1.configurationSet(configurationValue: [51,51], parameterNumber: 14, size: 2)) // 4x RGBW (RAINBOW)\n    //cmds << response(zwave.configurationV1.configurationGet(parameterNumber: 14))\n\n    // Get Basic:\n    //cmds << response(zwave.basicV1.basicGet().format())\n\n    // Get level (aggregate - channel 0):\n    //cmds << response(zwave.switchMultilevelV2.switchMultilevelGet()).format()) // Returns a SwitchMultilevelReport.\n    // OR\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:0).encapsulate(zwave.switchMultilevelV2.switchMultilevelGet()).format())\n\n    // Get level (individual channels):\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:2).encapsulate(zwave.switchMultilevelV2.switchMultilevelGet()).format())\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:3).encapsulate(zwave.switchMultilevelV2.switchMultilevelGet()).format())\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:4).encapsulate(zwave.switchMultilevelV2.switchMultilevelGet()).format())\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:5).encapsulate(zwave.switchMultilevelV2.switchMultilevelGet()).format())\n\n    // Set level (individual channels):\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:2).encapsulate(zwave.switchMultilevelV2.switchMultilevelSet(value: 0x00)).format())\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:3).encapsulate(zwave.switchMultilevelV2.switchMultilevelSet(value: 0x00)).format())\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:4).encapsulate(zwave.switchMultilevelV2.switchMultilevelSet(value: 0x00)).format())\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:5).encapsulate(zwave.switchMultilevelV2.switchMultilevelSet(value: 0x00)).format())\n\n    // Using the Switch Color Command Class:\n    // See: https://community.smartthings.com/t/color-switch-z-wave-command-class/19300\n    // switchColorSet allows you to send level for each colour channel in one command. It doesn't affect the channels not specified.\n    // The Fibaro RGBW returns SwitchMultilevelReport for each channel affected, so unfortunately, you don't get a single report back.\n\n    //cmds << response(zwave.switchColorV3.switchColorSet(red: 0xFF, green: 0xFF, blue: 0xFF, warmWhite: 0, coldWhite: 0).format()) // Set all colours.\n    //cmds << response(zwave.switchColorV3.switchColorSet(red: 128).format()) // Sets just the red channel.\n\n    // SwitchColour reports can only be requested for one colour at a time though:\n    //cmds << response(zwave.switchColorV3.switchColorGet().format()) // Returns report for warmWhite by default: SwitchColorReport(colorComponent: warmWhite, colorComponentId: 0, value: 161)\n    //cmds << response(zwave.switchColorV3.switchColorGet(colorComponent: \"red\").format()) // This should return a SwitchColorReport, however there appears to be a bug in the command class which causes an error.\n    // To get round the bug, we can make the request using the colorComponentId instead:\n    //cmds << response(zwave.switchColorV3.switchColorGet(colorComponentId: 2).format()) // Returns SwitchColorReport(colorComponent: red, colorComponentId: 2, value: 95)\n    //cmds << response(zwave.switchColorV3.switchColorGet(colorComponentId: 3).format()) // Returns SwitchColorReport(colorComponent: green, colorComponentId: 3, value: 0)\n    //cmds << response(zwave.switchColorV3.switchColorGet(colorComponentId: 4).format()) // Returns SwitchColorReport(colorComponent: blue, colorComponentId: 4, value: 0)\n    //cmds << response(zwave.switchColorV3.switchColorGet(colorComponentId: 0).format()) // Returns SwitchColorReport(colorComponent: warmWhite, colorComponentId: 0, value: 161)\n\n    // Get Meter Reports (aggregate values):\n    //cmds << response(zwave.meterV3.meterGet(scale: 0).format()) // Get energy meter report.\n    //cmds << response(zwave.meterV3.meterGet(scale: 2).format()) // Get power meter report.\n    //cmds << response(zwave.meterV3.meterReset().format()) // Reset accumulated energy.\n\n    // Get Meter Reports (individual channels): [DOES NOT APPEAR TO BE SUPPORTED BY THE FIBARO RGBW CONTROLLER]\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:3).encapsulate(zwave.meterV3.meterGet(scale: 0)).format()) // Get energy meter report for channel #3 - NO RESPONSE\n    //cmds << response(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:5).encapsulate(zwave.meterV3.meterGet(scale: 2)).format()) // Get power meter report for channel #5 - NO RESPONSE\n\n    // Get a MultiChannelEndPointReport:\n    //cmds << response(zwave.multiChannelV3.multiChannelEndPointGet())\n    //  This returns: MultiChannelEndPointReport(dynamic: false, endPoints: 5, identical: true, res00: 0, res11: false) - which basically just tells us there's 5 static endPoints.\n\n    // Get SensorMultilevelReport:\n    //cmds << response(zwave.sensorMultilevelV3.sensorMultilevelGet().format()) // Returns one report for sensorType == 4 (Instantaneous Power).\n\n    // Get CONFIGURATION reports (must specify a parameterNumber):\n    //cmds << response(zwave.configurationV1.configurationGet(parameterNumber: 10))\n    //cmds << response(zwave.configurationV1.configurationGet(parameterNumber: 12))\n    // There doesn't seem to be a way to request all Parameters in one go.\n\n    // Association Group Set/Get:\n    //cmds << response(zwave.associationV2.associationSet(groupingIdentifier:4, nodeId:[zwaveHubNodeId]).format()) // This adds the controller to Assoc. Group 4.\n    //cmds << response(zwave.associationV2.associationGet(groupingIdentifier:4).format())\n\n    // Get Manufaturer, Version, and Firmware reports.\n    //cmds << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())\n    //cmds << response(zwave.versionV1.versionGet().format())\n    //cmds << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format())\n\n    return delayBetween(cmds,200)\n}\n"
  },
  {
    "path": "devices/greenwave-powernode-single/README.md",
    "content": "# GreenWave PowerNode (Single) (NS210-G-EN)\nhttps://github.com/codersaur/SmartThings/tree/master/devices/greenwave-powernode-single\n\nCopyright (c) [David Lomas](https://github.com/codersaur)\n\n## Overview\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-main.png\" width=\"200\" align=\"right\">\nAn advanced SmartThings device handler for the GreenWave PowerNode (Single socket) Z-Wave power outlet. Firmware versions 4.23 / 4.28.\n\n### Key features:\n* Instantaneous _Power_ and Accumulated _Energy_ reporting.\n* _Room Colour_ indicator tile.\n* _Blink_ function for easy identification of the physical power outlet.\n* Physical and RF protection modes can be configured using the SmartThings GUI.\n* _Sync_ tile indicates when all configuration options are successfully synchronised with the physical device.\n* _Fault_ tile indicates overload / hardware errors.\n* All Z-Wave parameters can be configured using the SmartThings GUI.\n* Auto-off timer function.\n* Logger functionality enables critical errors and warnings to be saved to the _logMessage_ attribute.\n* Extensive inline code comments to support community development.\n\n## Installation\n\n1. Follow [these instructions](https://github.com/codersaur/SmartThings#device-handler-installation-procedure) to install the device handler in the SmartThings IDE.\n\n2. From the SmartThings app on your phone, edit the device settings to suit your installation and hit _Done_.\n\n## Settings\n\n#### General Settings:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-settings-general.png\" width=\"200\" align=\"right\">\n\n* **IDE Live Logging Level**: Set the level of log messages shown in the SmartThings IDE _Live Logging_ tab. For normal operation _Info_ or _Warning_ is recommended, if troubleshooting use _Debug_ or _Trace_.\n\n* **Device Logging Level**: Set the level of log messages that will be recorded in the device's _logMessage_ attribute. This offers a way to review historical messages without having to keep the IDE _Live Logging_ screen open. To prevent excessive events, the maximum level supported is _Warning_.\n\n* **Force Full Sync**: By default, only settings that have been modified will be synchronised with the device. Enable this setting to force all device parameters and association groups to be re-sent to the device.\n\n* **Timer Function (Auto-off)**: Automatically switch off the device after a specified time. Note, this is scheduled in SmartThings and is not a native function of the physical device.\n\n* **Ignore Current Leakage Alarms**: The PowerNode is eager to raise current leakage alarms. Enable this setting to ignore them.\n\n* **ALL ON/ALL OFF Function**: Control the device's response to SWITCH_ALL_SET commands.\n\n#### Device Parameters:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-settings-params.png\" width=\"200\" align=\"right\">\n\nThe settings in this section can be used to specify the value of all writable device parameters. It is recommended to consult the manufacturer's manual for a full description of each parameter.\n\nIf no value is specified for a parameter, then it will not be synched with the device and the existing value in the device will be preserved.\n\n#### Power Report Threshold:\nDetermines the percentage change in power consumption that will trigger a report to be sent by the device. **IMPORTANT: Be careful not to set this value too low, as the device will send reports every second causing network congestion!** It is recommended to use a value between 30% and 50%.\n\n#### Keep-Alive Time:\nIt is recommended to set this setting to 255 minutes to prevent the _Circle LED_ from flashing.\n\n#### State After Power Failure:\nDetermine the power state to be restored after a power failure. **Only supported with firmware v4.28+**\n\n#### LED for Network Error:\nDetermine if the LED will indicate network errors. **Only supported with firmware v4.28+**\n\n## GUI\n\n#### Main Tile:\nThe main tile indicates the switch state. Tap it to toggle the switch on and off.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-main.png\" width=\"200\">\n\n#### Power and Energy Tiles:\nThese tiles display the instantaneous power consumption of the device (Watts) and the accumulated energy consumption (KWh). The _Now:_ tile can be tapped to force the device state to be refreshed. The _Since: ..._ tile can be tapped to reset the _Accumulated Energy_ figure.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-power-energy.png\" width=\"200\">\n\n#### Room Colour Wheel Tile:\nThis tile mirrors the _Room Colour Wheel_ on the bottom right of the physical power outlet.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-colour-wheel-aqua.png\" width=\"100\"> <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-colour-wheel-orange.png\" width=\"100\">\n\n#### Blink Tile:\nThe _Blink_ tile will cause the _Circle LED_ on the outlet to blink for ~20 seconds. This is useful to identify the physical device.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-blink.png\" width=\"100\">\n\n#### Local Protection Tile:\nThis tile toggles the _local protection_ state. This can be used to prevent unintentional control (e.g. by a child), by disabling the physical power switch on the device.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-lp-unprotected.png\" width=\"100\"> <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-lp-protected.png\" width=\"100\">\n\n#### RF Protection Tile:\nThis tile toggles the _RF protection_ state. Enabling _RF Protection_ means the device will not respond to wireless commands from other Z-Wave devices, including on/off commands issued via the SmartThings app.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-rfp-unprotected.png\" width=\"100\"> <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-rfp-protected.png\" width=\"100\">\n\n#### Sync Tile:\nThis tile indicates when all configuration settings have been successfully synchronised with the physical device.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-synced.png\" width=\"100\"> <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-sync-pending.png\" width=\"100\">\n\n#### Fault Tile:\nThe _Fault_ tile indicates if the device has reported any faults. These may include load faults, firmware, or hardware issues. Once any faults have been investigated and remediated, the tile can be tapped to clear the fault status.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-clear.png\" width=\"100\"> <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-fault-active.png\" width=\"100\">\n\n## SmartApp Integration\n\n#### Attributes:\n\nThe device handler publishes the following attributes:\n\n* **switch [ENUM]**: Switch status [_on_, _off_].\n* **power [NUMBER]**: Instantaneous power consumption (Watts).\n* **energy [NUMBER]**: Accumulated energy consumption (kWh).\n* **energyLastReset [STRING]**: Last time _Accumulated Energy_ was reset.\n* **fault [STRING]**: Indicates if the device has any faults. '_clear_' if no active faults.\n* **localProtectionMode [ENUM]**: Physical protection mode [_unprotected_, _sequence_, _noControl_].\n* **rfProtectionMode [ENUM]**: Wireless protection mode [_unprotected_, _noControl_, _noResponse_].\n* **logMessage [STRING]**: Important log messages.\n* **syncPending [NUMBER]**: The number of configuration items that need to be synced with the physical device. _0_ if the device is fully synchronised.\n* **wheelStatus [ENUM]**: Status of the _Room Colour Wheel_ [_black_, _white_, _green_, ...]\n\n#### Commands:\n\nThe device exposes the following commands which can be called from a SmartApp:\n\n* **on()**: Turn the switch on.\n* **off()**: Turn the switch off.\n* **refresh()**: Refresh device state.\n* **resetTamper()**: Clear any tamper alerts.\n* **blink()**: Causes the Circle LED to blink for ~20 seconds.\n* **reset()**: Alias for _resetEnergy()_.\n* **resetEnergy()**:  Reset accumulated energy figure to 0.\n* **resetFault()**: Reset fault alarm to 'clear'.\n* **setLocalProtectionMode()**: Set physical protection mode.\n* **toggleLocalProtectionMode()**: Toggle physical protection mode.\n* **setRfProtectionMode()**: Set wireless protection mode.\n* **toggleRfProtectionMode()**: Toggle wireless protection mode.\n\n## Version History\n\n#### 2017-03-08: v1.01\n  *  getParamsMd(): set fwVersion to 4.22, for parameters #0,#1, and #2.\n\n#### 2017-03-05: v1.00\n  *  Initial version.\n\n## Physical Device Notes\n\nGeneral notes concerning the GreenWave PowerNode:\n\n* The device is generally poor at reporting physical switch events (reports are typically delayed by 10-20s). To work round the issue, this device handler will request BinarySwitchReports if a meter report indicates that there has been a change in state.\n* The device seems to send a lot of Meter Reports. It is important to be cautious setting parameter #0, to avoid spamming the Z-Wave network. Ideally, there should be a parameter to control the _power reporting interval_.\n* The device reports _Current Leakage_ alarms frequently, hence this device handler has an option to ignore them.\n* There does not appear to be any way (in software) to turn off the white power button LED, so this device isn't great for use in bedrooms as it lights up dark rooms. If anyone has a solution, please let me know!\n\n## References\n Some useful links relevant to the development of this device handler:\n* [GreenWave PowerNode - Z-Wave certification information](http://products.z-wavealliance.org/products/629)\n\n## License\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n"
  },
  {
    "path": "devices/greenwave-powernode-single/greenwave-powernode-single.groovy",
    "content": "/*****************************************************************************************************************\n *  Copyright: David Lomas (codersaur)\n *\n *  Name: GreenWave PowerNode (Single) Advanced\n *\n *  Date: 2017-03-08\n *\n *  Version: 1.01\n *\n *  Source: https://github.com/codersaur/SmartThings/tree/master/devices/greenwave-powernode-single\n *\n *  Author: David Lomas (codersaur)\n *\n *  Description: An advanced SmartThings device handler for the GreenWave PowerNode (Single socket) Z-Wave power outlet.\n *\n *  For full information, including installation instructions, exmples, and version history, see:\n *   https://github.com/codersaur/SmartThings/tree/master/devices/greenwave-powernode-single\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n *****************************************************************************************************************/\nmetadata {\n    definition (name: \"GreenWave PowerNode (Single) Advanced\", namespace: \"codersaur\", author: \"David Lomas\") {\n        capability \"Actuator\"\n        capability \"Switch\"\n        capability \"Sensor\"\n        capability \"Energy Meter\"\n        capability \"Power Meter\"\n        capability \"Polling\"\n        capability \"Refresh\"\n\n        // Custom (Virtual) Capabilities:\n        //capability \"Fault\"\n        //capability \"Logging\"\n        //capability \"Protection\"\n\n        // Standard Attributes:\n        attribute \"switch\", \"enum\", [\"on\", \"off\"]\n        attribute \"power\", \"number\"\n        attribute \"energy\", \"number\"\n\n        // Custom Attributes:\n        attribute \"energyLastReset\", \"string\"   // Last time Accumulated Engergy was reset.\n        attribute \"fault\", \"string\"             // Indicates if the device has any faults. 'clear' if no active faults.\n        attribute \"localProtectionMode\", \"enum\", [\"unprotected\",\"sequence\",\"noControl\"] // Physical protection mode.\n        attribute \"rfProtectionMode\", \"enum\", [\"unprotected\",\"noControl\",\"noResponse\"] // Wireless protection mode.\n        attribute \"logMessage\", \"string\"        // Important log messages.\n        attribute \"syncPending\", \"number\"       // Number of config items that need to be synced with the physical device.\n        attribute \"wheelStatus\", \"enum\", [\"black\",\"green\",\"blue\",\"red\",\"yellow\",\"violet\",\"orange\",\"aqua\",\"pink\",\"white\"]\n\n        // Display Attributes:\n        // These are only required because the UI lacks number formatting and strips leading zeros.\n        attribute \"dispEnergy\", \"string\"\n        attribute \"dispPower\", \"string\"\n\n        // Custom Commands:\n        command \"blink\"                     // Causes the Circle LED to blink for ~20 seconds.\n        command \"reset\"                     // Alias for resetEnergy().\n        command \"resetEnergy\"               // Reset accumulated energy figure to 0.\n        command \"resetFault\"                // Reset fault alarm to 'clear'.\n        command \"setLocalProtectionMode\"    // Set physical protection mode.\n        command \"toggleLocalProtectionMode\" // Toggle physical protection mode.\n        command \"setRfProtectionMode\"       // Set wireless protection mode.\n        command \"toggleRfProtectionMode\"    // Toggle wireless protection mode.\n        command \"sync\"                      // Sync configuration with physical device.\n        command \"test\"                      // Test function.\n\n        // Fingerprints:\n        fingerprint mfr: \"0099\", prod: \"0002\", model: \"0002\"\n        fingerprint type: \"1001\", mfr: \"0099\", cc: \"20,25,27,32,56,70,71,72,75,85,86,87\"\n        fingerprint inClusters: \"0x20,0x25,0x27,0x32,0x56,0x70,0x71,0x72,0x75,0x85,0x86,0x87\"\n    }\n\n    tiles(scale: 2) {\n        // Multi Tile:\n        multiAttributeTile(name:\"switch\", type: \"generic\", width: 6, height: 4, canChangeIcon: true) {\n            tileAttribute (\"switch\", key: \"PRIMARY_CONTROL\") {\n                attributeState \"on\", label:'${name}', action:\"off\", icon:\"st.switches.switch.on\", backgroundColor:\"#79b821\", nextState:\"turningOff\"\n                attributeState \"off\", label:'${name}', action:\"on\", icon:\"st.switches.switch.off\", backgroundColor:\"#ffffff\", nextState:\"turningOn\"\n                attributeState \"turningOn\", label:'${name}', action:\"off\", icon:\"st.switches.switch.on\", backgroundColor:\"#79b821\", nextState:\"turningOff\"\n                attributeState \"turningOff\", label:'${name}', action:\"on\", icon:\"st.switches.switch.off\", backgroundColor:\"#ffffff\", nextState:\"turningOn\"\n            }\n        }\n\n        // Instantaneous Power:\n        valueTile(\"instMode\", \"dispPower\", decoration: \"flat\", width: 2, height: 1) {\n            state \"default\", label:'Now:', action:\"refresh\",\n            icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_refresh.png\"\n        }\n        valueTile(\"power\", \"dispPower\", decoration: \"flat\", width: 2, height: 1) {\n            state \"default\", label:'${currentValue}',\n            icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n        }\n\n        // Accumulated Energy:\n        valueTile(\"energyLastReset\", \"energyLastReset\", decoration: \"flat\", width: 2, height: 1) {\n            state \"default\", label:'Since:  ${currentValue}', action:\"resetEnergy\",\n            icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_stopwatch_reset.png\"\n        }\n        valueTile(\"energy\", \"dispEnergy\", decoration: \"flat\", width: 2, height: 1) {\n            state \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n        }\n\n        // Other Tiles:\n        standardTile(\"wheelStatus\", \"wheelStatus\", decoration: \"flat\", width: 2, height: 2) {\n            state \"black\", label:'${currentValue}', backgroundColor:\"#000000\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"green\", label:'${currentValue}', backgroundColor:\"#009933\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"blue\", label:'${currentValue}', backgroundColor:\"#0033CC\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"red\", label:'${currentValue}', backgroundColor:\"#FF0000\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"yellow\", label:'${currentValue}', backgroundColor:\"#EEEE00\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"violet\", label:'${currentValue}', backgroundColor:\"#9900FF\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"orange\", label:'${currentValue}', backgroundColor:\"#FF9933\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"aqua\", label:'${currentValue}', backgroundColor:\"#33CCFF\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"pink\", label:'${currentValue}', backgroundColor:\"#FF99FF\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_blank.png\"\n            state \"white\", label:'${currentValue}', backgroundColor:\"#EEEEEE\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_padlock_closed.png\"\n        }\n        standardTile(\"blink\", \"blink\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Blink', action:\"blink\", icon:\"st.illuminance.illuminance.light\"\n        }\n        standardTile(\"localProtectionMode\", \"localProtectionMode\", decoration: \"flat\", width: 2, height: 2) {\n            state \"unprotected\", label:'Unprotected', action:\"toggleLocalProtectionMode\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_padlock_open.png\"\n            state \"sequence\", label:'Sequence', action:\"toggleLocalProtectionMode\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_padlock_buttons_closed.png\"\n            state \"noControl\", label:'Protected', action:\"toggleLocalProtectionMode\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_padlock_closed.png\"\n        }\n        standardTile(\"rfProtectionMode\", \"rfProtectionMode\", decoration: \"flat\", width: 2, height: 2) {\n            state \"unprotected\", label:'Unprotected', action:\"toggleRfProtectionMode\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_padlock_wireless_open.png\"\n            state \"noControl\", label:'Protected', action:\"toggleRfProtectionMode\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_padlock_wireless_closed.png\"\n            state \"noResponse\", label:'Protected (NR)', action:\"toggleRfProtectionMode\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_padlock_wireless_closed.png\"\n        }\n        standardTile(\"syncPending\", \"syncPending\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Sync Pending', backgroundColor:\"#FF6600\", action:\"sync\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_cycle.png\"\n            state \"0\", label:'Synced', backgroundColor:\"#79b821\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_tick.png\"\n        }\n        standardTile(\"refresh\", \"refresh\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'', action:\"refresh\", icon:\"st.secondary.refresh\"\n        }\n        standardTile(\"fault\", \"fault\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'${currentValue} Fault', action:\"resetFault\", backgroundColor:\"#FF6600\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_warn.png\"\n            state \"clear\", label:'${currentValue}', action:\"\", backgroundColor:\"#79b821\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_tick.png\"\n        }\n        standardTile(\"test\", \"test\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Test', action:\"test\", icon:\"st.secondary.tools\"\n        }\n\n        main([\"switch\"])\n        details([\n            \"switch\",\n            \"instMode\",\"power\",\n            \"wheelStatus\",\n            \"energyLastReset\",\"energy\",\n            \"blink\",\n            \"localProtectionMode\",\n            \"rfProtectionMode\",\n            \"syncPending\",\n//            \"refresh\",\n            \"fault\"//,\n            //\"test\"\n        ])\n    }\n\n    preferences {\n\n        section { // GENERAL:\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"GENERAL:\",\n                description: \"General device handler settings.\"\n            )\n\n            input (\n                name: \"configLoggingLevelIDE\",\n                title: \"IDE Live Logging Level:\\nMessages with this level and higher will be logged to the IDE.\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"None\",\n                    \"1\" : \"Error\",\n                    \"2\" : \"Warning\",\n                    \"3\" : \"Info\",\n                    \"4\" : \"Debug\",\n                    \"5\" : \"Trace\"\n                ],\n                defaultValue: \"3\",\n                required: false\n            )\n\n            input (\n                name: \"configLoggingLevelDevice\",\n                title: \"Device Logging Level:\\nMessages with this level and higher will be logged to the logMessage attribute.\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"None\",\n                    \"1\" : \"Error\",\n                    \"2\" : \"Warning\"\n                ],\n                defaultValue: \"2\",\n                required: false\n            )\n\n            input (\n                name: \"configSyncAll\",\n                title: \"Force Full Sync:\\nAll device settings will be re-sent to the device.\",\n                type: \"boolean\",\n                defaultValue: false,\n                required: false\n            )\n\n            input (\n                name: \"configAutoOffTime\",\n                title: \"Timer Function (Auto-off):\\nAutomatically switch off the device after a specified time.\\n\" +\n                \"Values:\\n0 = Function Disabled\\n1-86400 = time in seconds\\nDefault Value: 0\",\n                type: \"number\",\n                range: \"0..86400\",\n                defaultValue: 0,\n                required: false\n            )\n\n            input (\n                name: \"configIgnoreCurrentLeakageAlarms\",\n                title: \"Ignore Current Leakage Alarms:\\nDo not raise a fault on a current leakage alarm.\",\n                type: \"boolean\",\n                defaultValue: false,\n                required: false\n            )\n\n            input (\n                name: \"configSwitchAllMode\",\n                title: \"ALL ON/ALL OFF Function:\\nResponse to SWITCH_ALL_SET commands.\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"0: All ON not active, All OFF not active\",\n                    \"1\" : \"1: All ON not active, All OFF active\",\n                    \"2\" : \"2: All ON active, All OFF not active\",\n                    \"255\" : \"255: All ON active, All OFF active\"],\n                defaultValue: \"255\",\n                required: false\n            )\n\n        }\n\n        generatePrefsParams()\n\n        //generatePrefsAssocGroups() // All Assoc Groups are HubOnly for this device.\n\n    }\n\n}\n\n/*****************************************************************************************************************\n *  SmartThings System Commands:\n *****************************************************************************************************************/\n\n/**\n *  installed()\n *\n *  Runs when the device is first installed.\n *\n *  Action: Set initial values for internal state, and request MSR/Version reports.\n **/\ndef installed() {\n    log.trace \"installed()\"\n\n    state.installedAt = now()\n    state.energyLastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    state.loggingLevelIDE     = 3\n    state.loggingLevelDevice  = 2\n    state.useSecurity = false\n    state.useCrc16 = true\n    state.fwVersion = 4.23 // Will be updated when versionReport is received.\n    state.protectLocalTarget = 0\n    state.protectRfTarget = 0\n    state.autoOffTime = 0\n\n    sendEvent(name: \"fault\", value: \"clear\", displayed: false)\n\n    def cmds = []\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, scaledConfigurationValue: 255) // Set Keep-Alive to 255.\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 2) // Wheel Status\n    cmds << zwave.protectionV2.protectionGet()\n    cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet()\n    cmds << zwave.versionV1.versionGet()\n\n    sendCommands(cmds)\n}\n\n/**\n *  updated()\n *\n *  Runs when the user hits \"Done\" from Settings page.\n *\n *  Action: Process new settings, sync parameters and association group members with the physical device.\n *\n *  Note: Weirdly, update() seems to be called twice. So execution is aborted if there was a previous execution\n *  within two seconds. See: https://community.smartthings.com/t/updated-being-called-twice/62912\n **/\ndef updated() {\n    logger(\"updated()\",\"trace\")\n\n    def cmds = []\n\n    if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {\n        state.updatedLastRanAt = now()\n\n        // Update internal state:\n        state.loggingLevelIDE     = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3\n        state.loggingLevelDevice  = (settings.configLoggingLevelDevice) ? settings.configLoggingLevelDevice.toInteger(): 2\n        state.syncAll             = (\"true\" == settings.configSyncAll)\n        state.autoOffTime         = (settings.configAutoOffTime) ? settings.configAutoOffTime.toInteger() : 0\n        state.ignoreCurrentLeakageAlarms = (\"true\" == settings.configIgnoreCurrentLeakageAlarms)\n        state.switchAllModeTarget = (settings.configSwitchAllMode) ? settings.configSwitchAllMode.toInteger() : 255\n\n        // Update Parameter target values:\n        getParamsMd().findAll( { !it.readonly & (it.fwVersion <= state.fwVersion) } ).each { // Exclude readonly/newer parameters.\n            state.\"paramTarget${it.id}\" = settings.\"configParam${it.id}\"?.toInteger()\n        }\n\n        // Update Assoc Group target values:\n        getAssocGroupsMd().findAll( { !it.hubOnly } ).each {\n            state.\"assocGroupTarget${it.id}\" = parseAssocGroupInput(settings.\"configAssocGroup${it.id}\", it.maxNodes)\n        }\n        getAssocGroupsMd().findAll( { it.hubOnly } ).each {\n            state.\"assocGroupTarget${it.id}\" = [ zwaveHubNodeId ]\n        }\n\n        // Sync configuration with phyiscal device:\n        sync(state.syncAll)\n\n        // Request device medadata (this just seems the best place to do it):\n        cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet()\n        cmds << zwave.versionV1.versionGet()\n\n        return sendCommands(cmds)\n    }\n    else {\n        logger(\"updated(): Ran within last 2 seconds so aborting.\",\"debug\")\n    }\n}\n\n/**\n *  parse()\n *\n *  Called when messages from the device are received by the hub. The parse method is responsible for interpreting\n *  those messages and returning event definitions (and command responses).\n *\n *  As this is a Z-wave device, zwave.parse() is used to convert the message into a command. The command is then\n *  passed to zwaveEvent(), which is overloaded for each type of command below.\n *\n *  Note: There is no longer any need to check if description == \"updated\".\n *\n *  Parameters:\n *   String      description        The raw message from the device.\n **/\ndef parse(description) {\n    logger(\"parse(): Parsing raw message: ${description}\",\"trace\")\n\n    def result = []\n\n    def cmd = zwave.parse(description, getCommandClassVersions())\n    if (cmd) {\n        result += zwaveEvent(cmd)\n    } else {\n        logger(\"parse(): Could not parse raw message: ${description}\",\"error\")\n    }\n    return result\n}\n\n/*****************************************************************************************************************\n *  Z-wave Event Handlers.\n *****************************************************************************************************************/\n\n/**\n *  zwaveEvent( COMMAND_CLASS_BASIC (0x20) : BASIC_REPORT (0x03) )\n *\n *  The Basic Report command is used to advertise the status of the primary functionality of the device.\n *\n *  Action: Raise switch event and log an info message if state has changed.\n *    Schedule autoOff() if an autoOffTime is configured.\n *\n *  cmd attributes:\n *    Short    value\n *      0x00       = Off\n *      0x01..0x63 = 0..100%\n *      0xFE       = Unknown\n *      0xFF       = On\n *\n *  Example: BasicReport(value: 255)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {\n    logger(\"zwaveEvent(): Basic Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    def switchValue = (cmd.value ? \"on\" : \"off\")\n    def switchEvent = createEvent(name: \"switch\", value: switchValue)\n    if (switchEvent.isStateChange) logger(\"Switch turned ${switchValue}.\",\"info\")\n    result << switchEvent\n\n    if ( switchEvent.isStateChange & (switchValue == \"on\") & (state.autoOffTime > 0) ) {\n        logger(\"Scheduling Auto-off in ${state.autoOffTime} seconds.\",\"info\")\n        runIn(state.autoOffTime,autoOff)\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_APPLICATION_STATUS (0x22) : APPLICATION_BUSY (0x01) )\n *\n *  The Application Busy command used to instruct a node that the node that it is trying to communicate with is\n *  busy and is unable to service the request right now.\n *\n *  Action: Log a warning message.\n *\n *  cmd attributes:\n *    Short  status\n *      0  =  Try again later.\n *      1  =  Try again in Wait Time seconds.\n *      2  =  Request queued, executed later.\n *    Short  waitTime  Number of seconds to wait before retrying.\n *\n *  Example: ApplicationBusy(status: 0, waitTime: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {\n    logger(\"zwaveEvent(): Application Busy received: ${cmd}\",\"trace\")\n\n    switch(cmd.status) {\n        case 0:\n        logger(\"Device is busy. Try again later.\",\"warn\")\n        break\n        case 1:\n        logger(\"Device is busy. Retry in ${cmd.waitTime} seconds.\",\"warn\")\n        break\n        case 2:\n        logger(\"Device is busy. Request is queued.\",\"warn\")\n        break\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_APPLICATION_STATUS (0x22) : APPLICATION_REJECTED_REQUEST (0x02) )\n *\n *  The Application Rejected Request command used to instruct a node that a command was rejected by the receiving node.\n *\n *  Action: Log a warning message.\n *\n *  Note: These will be received if rfProtectionMode is 'No Control'.\n *\n *  cmd attributes:\n *    Short  status  Always 0.\n *\n *  Example: ApplicationRejectedRequest(status: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {\n    //logger(\"zwaveEvent(): Application Rejected Request received: ${cmd}\",\"trace\")\n    logger(\"A command was rejected. Most likely, RF Protection Mode is set to 'No Control'.\",\"warn\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_BINARY (0x25) : SWITCH_BINARY_REPORT (0x03) )\n *\n *  The Binary Switch Report command  is used to advertise the status of a device with On/Off or Enable/Disable\n *  capability.\n *\n *  Action: Raise switch event and log an info message if state has changed.\n *    Schedule autoOff() if an autoOffTime is configured.\n *\n *  cmd attributes:\n *    Short   value   0xFF for on, 0x00 for off\n *\n *  Example: SwitchBinaryReport(value: 255)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {\n    logger(\"zwaveEvent(): Switch Binary Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    def switchValue = (cmd.value ? \"on\" : \"off\")\n    def switchEvent = createEvent(name: \"switch\", value: switchValue)\n    if (switchEvent.isStateChange) logger(\"Switch turned ${switchValue}.\",\"info\")\n    result << switchEvent\n\n    if ( switchEvent.isStateChange & (switchValue == \"on\") & (state.autoOffTime > 0) ) {\n        logger(\"Scheduling Auto-off in ${state.autoOffTime} seconds.\",\"info\")\n        runIn(state.autoOffTime,autoOff)\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_ALL (0x27) : SWITCH_ALL_REPORT (0x03) )\n *\n *  The All Switch Report Command is used to report if the device is included or excluded from the all on/all off\n *  functionality.\n *\n *  Action: Cache value, update syncPending, and log an info message.\n *\n *  cmd attributes:\n *    Short    mode\n *      0   = MODE_EXCLUDED_FROM_THE_ALL_ON_ALL_OFF_FUNCTIONALITY\n *      1   = MODE_EXCLUDED_FROM_THE_ALL_ON_FUNCTIONALITY_BUT_NOT_ALL_OFF\n *      2   = MODE_EXCLUDED_FROM_THE_ALL_OFF_FUNCTIONALITY_BUT_NOT_ALL_ON\n *      255 = MODE_INCLUDED_IN_THE_ALL_ON_ALL_OFF_FUNCTIONALITY\n *\n *  Example: SwitchAllReport(mode: 255)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchallv1.SwitchAllReport cmd) {\n    logger(\"zwaveEvent(): Switch All Report received: ${cmd}\",\"trace\")\n\n    state.switchAllModeCache = cmd.mode\n\n    def msg = \"\"\n    switch (cmd.mode) {\n            case 0:\n                msg = \"Device is excluded from the all on/all off functionality.\"\n                break\n\n            case 1:\n                msg = \"Device is excluded from the all on functionality but not all off.\"\n                break\n\n            case 2:\n                msg = \"Device is excluded from the all off functionality but not all on.\"\n                break\n\n            default:\n                msg = \"Device is included in the all on/all off functionality.\"\n                break\n    }\n    logger(\"Switch All Mode: ${msg}\",\"info\")\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_METER_V3 (0x32) : METER_REPORT_V3 (0x02) )\n *\n *  The Meter Report Command is used to advertise a meter reading.\n *\n *  Action: Raise appropriate type of event (and disp... event) and log an info message.\n *   Plus, request a Switch Binary Report if power report suggests switch state has changed.\n *   (This is necessary because the PowerNode does not report physical switch events reliably).\n *\n *  Note: GreenWave PowerNode supports energy and power reporting only.\n *\n *  cmd attributes:\n *    Integer        deltaTime                   Time in seconds since last report.\n *    Short          meterType                   Specifies the type of metering device.\n *      0x00 = Unknown\n *      0x01 = Electric meter\n *      0x02 = Gas meter\n *      0x03 = Water meter\n *    List<Short>    meterValue                  Meter value as an array of bytes.\n *    Double         scaledMeterValue            Meter value as a double.\n *    List<Short>    previousMeterValue          Previous meter value as an array of bytes.\n *    Double         scaledPreviousMeterValue    Previous meter value as a double.\n *    Short          size                        The size of the array for the meterValue and previousMeterValue.\n *    Short          scale                       Indicates what unit the sensor uses (dependent on meterType).\n *    Short          precision                   The decimal precision of the values.\n *    Short          rateType                    Specifies if it is import or export values to be read.\n *      0x01 = Import (consumed)\n *      0x02 = Export (produced)\n *    Boolean        scale2                      ???\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {\n    logger(\"zwaveEvent(): Meter Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    switch (cmd.meterType) {\n        case 1:  // Electric meter:\n            switch (cmd.scale) {\n                case 0:  // Accumulated Energy (kWh):\n                    result << createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kWh\", displayed: true)\n                    result << createEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" kWh\", displayed: false)\n                    logger(\"New meter reading: Accumulated Energy: ${cmd.scaledMeterValue} kWh\",\"info\")\n                    break\n\n                case 1:  // Accumulated Energy (kVAh):\n                    result << createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kVAh\", displayed: true)\n                    result << createEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" kVAh\", displayed: false)\n                    logger(\"New meter reading: Accumulated Energy: ${cmd.scaledMeterValue} kVAh\",\"info\")\n                    break\n\n                case 2:  // Instantaneous Power (Watts):\n                    result << createEvent(name: \"power\", value: cmd.scaledMeterValue, unit: \"W\", displayed: true)\n                    result << createEvent(name: \"dispPower\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" W\", displayed: false)\n                    logger(\"New meter reading: Instantaneous Power: ${cmd.scaledMeterValue} W\",\"info\")\n\n                    // Request Switch Binary Report if power suggests switch state has changed:\n                    def sw = (cmd.scaledMeterValue) ? \"on\" : \"off\"\n                    if ( device.latestValue(\"switch\") != sw) { result << prepCommands([zwave.switchBinaryV1.switchBinaryGet()]) }\n                    break\n\n                case 3:  // Accumulated Pulse Count:\n                    result << createEvent(name: \"pulseCount\", value: cmd.scaledMeterValue, unit: \"\", displayed: true)\n                    logger(\"New meter reading: Accumulated Electricity Pulse Count: ${cmd.scaledMeterValue}\",\"info\")\n                    break\n\n                case 4:  // Instantaneous Voltage (Volts):\n                    result << createEvent(name: \"voltage\", value: cmd.scaledMeterValue, unit: \"V\", displayed: true)\n                    result << createEvent(name: \"dispVoltage\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" V\", displayed: false)\n                    logger(\"New meter reading: Instantaneous Voltage: ${cmd.scaledMeterValue} V\",\"info\")\n                    break\n\n                 case 5:  // Instantaneous Current (Amps):\n                    result << createEvent(name: \"current\", value: cmd.scaledMeterValue, unit: \"A\", displayed: true)\n                    result << createEvent(name: \"dispCurrent\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" V\", displayed: false)\n                    logger(\"New meter reading: Instantaneous Current: ${cmd.scaledMeterValue} A\",\"info\")\n                    break\n\n                 case 6:  // Instantaneous Power Factor:\n                    result << createEvent(name: \"powerFactor\", value: cmd.scaledMeterValue, unit: \"\", displayed: true)\n                    result << createEvent(name: \"dispPowerFactor\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal), displayed: false)\n                    logger(\"New meter reading: Instantaneous Power Factor: ${cmd.scaledMeterValue}\",\"info\")\n                    break\n\n                default:\n                    logger(\"zwaveEvent(): Meter Report with unhandled scale: ${cmd}\",\"warn\")\n                    break\n            }\n            break\n\n        default:\n            logger(\"zwaveEvent(): Meter Report with unhandled meterType: ${cmd}\",\"warn\")\n            break\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_CRC16_ENCAP (0x56) : CRC_16_ENCAP (0x01) )\n *\n *  The CRC-16 Encapsulation Command Class is used to encapsulate a command with an additional CRC-16 checksum\n *  to ensure integrity of the payload. The purpose for this command class is to ensure a higher integrity level\n *  of payloads carrying important data.\n *\n *  Action: Extract the encapsulated command and pass to zwaveEvent().\n *\n *  Note: Validation of the checksum is not necessary as this is performed by the hub.\n *\n *  cmd attributes:\n *    Integer      checksum      Checksum.\n *    Short        command       Command identifier of the embedded command.\n *    Short        commandClass  Command Class identifier of the embedded command.\n *    List<Short>  data          Embedded command data.\n *\n *  Example: Crc16Encap(checksum: 125, command: 2, commandClass: 50, data: [33, 68, 0, 0, 0, 194, 0, 0, 77])\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {\n    logger(\"zwaveEvent(): CRC-16 Encapsulation Command received: ${cmd}\",\"trace\")\n\n    def versions = getCommandClassVersions()\n    def version = versions[cmd.commandClass as Integer]\n    def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)\n    def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)\n    // TO DO: It should be possible to replace the lines above with this line soon...\n    //def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())\n    if (!encapsulatedCommand) {\n        logger(\"zwaveEvent(): Could not extract command from ${cmd}\",\"error\")\n    } else {\n        return zwaveEvent(encapsulatedCommand)\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_CONFIGURATION (0x70) : CONFIGURATION_REPORT (0x03) )\n *\n *  The Configuration Report Command is used to advertise the actual value of the advertised parameter.\n *\n *  Action: Store the value in the parameter cache, update syncPending, and log an info message.\n *   Update wheelStatus if parameter #2.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  cmd attributes:\n *    List<Short>  configurationValue  Value of parameter (byte array).\n *    Short        parameterNumber     Parameter ID.\n *    Short        size                Size of parameter's value (bytes).\n *\n *  Example: ConfigurationReport(configurationValue: [10], parameterNumber: 0, reserved11: 0,\n *            scaledConfigurationValue: 10, size: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {\n    logger(\"zwaveEvent(): Configuration Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    def paramMd = getParamsMd().find( { it.id == cmd.parameterNumber })\n    // Some values are treated as unsigned and some as signed, so we convert accordingly:\n    def paramValue = (paramMd?.isSigned) ? cmd.scaledConfigurationValue : byteArrayToUInt(cmd.configurationValue)\n    def signInfo = (paramMd?.isSigned) ? \"SIGNED\" : \"UNSIGNED\"\n\n    state.\"paramCache${cmd.parameterNumber}\" = paramValue\n    logger(\"Parameter #${cmd.parameterNumber} [${paramMd?.name}] has value: ${paramValue} [${signInfo}]\",\"info\")\n    updateSyncPending()\n\n    // Update wheelStatus if parameter #2:\n    if (cmd.parameterNumber == 2) {\n        def wheelStatus = getWheelColours()[paramValue]\n        def wheelEvent = createEvent(name: \"wheelStatus\", value: wheelStatus)\n        if (wheelEvent.isStateChange) logger(\"Room Colour Wheel changed to ${wheelStatus}.\",\"info\")\n        result << wheelEvent\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_ALARM (0x71) : ALARM_REPORT (0x05) )\n *\n *  The Alarm Report command used to report the type and level of an alarm.\n *\n *  Action: Raise a fault event and log a warning message.\n *\n *  Note: The GreenWave PowerNode seems especially eager to raise current leakage alarms, so there is an\n *  optional setting to ignore them.\n *\n *  cmd attributes:\n *    Short  alarmLevel  Application specific\n *    Short  alarmType   Application specific\n *\n *  Example: AlarmReport(alarmLevel: 1, alarmType: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) {\n    logger(\"zwaveEvent(): Alarm Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    switch(cmd.alarmType) {\n        case 1: // Current Leakage:\n            if (!state.ignoreCurrentLeakageAlarms) { result << createEvent(name: \"fault\", value: \"currentLeakage\",\n              descriptionText: \"Current Leakage detected!\", displayed: true) }\n            logger(\"Current Leakage detected!\",\"warn\")\n            break\n\n        // TO DO: Check other alarm codes.\n\n        default: // Over-current:\n            result << createEvent(name: \"fault\", value: \"current\", descriptionText: \"Over-current detected!\", displayed: true)\n            logger(\"Over-current detected!\",\"warn\")\n            break\n    }\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 (0x72) : MANUFACTURER_SPECIFIC_REPORT_V2 (0x05) )\n *\n *  Manufacturer-Specific Reports are used to advertise manufacturer-specific information, such as product number\n *  and serial number.\n *\n *  Action: Publish values as device 'data'. Log a warn message if manufacturerId and/or productId do not match.\n *\n *  Example: ManufacturerSpecificReport(manufacturerId: 153, manufacturerName: GreenWave Reality Inc.,\n *   productId: 2, productTypeId: 2)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {\n    logger(\"zwaveEvent(): Manufacturer-Specific Report received: ${cmd}\",\"trace\")\n\n    // Display as hex strings:\n    def manufacturerIdDisp = String.format(\"%04X\",cmd.manufacturerId)\n    def productIdDisp = String.format(\"%04X\",cmd.productId)\n    def productTypeIdDisp = String.format(\"%04X\",cmd.productTypeId)\n\n    logger(\"Manufacturer-Specific Report: Manufacturer ID: ${manufacturerIdDisp}, Manufacturer Name: ${cmd.manufacturerName}\" +\n    \", Product Type ID: ${productTypeIdDisp}, Product ID: ${productIdDisp}\",\"info\")\n\n    if ( 153 != cmd.manufacturerId) logger(\"Device Manufacturer is not GreenWave Reality. \" +\n      \"Using this device handler with a different device may damage your device!\",\"warn\")\n    if ( 2 != cmd.productId) logger(\"Product ID does not match GreenWave PowerNode (Single). \" +\n      \"Using this device handler with a different device may damage you device!\",\"warn\")\n\n    updateDataValue(\"manufacturerName\",cmd.manufacturerName)\n    updateDataValue(\"manufacturerId\",manufacturerIdDisp)\n    updateDataValue(\"productId\",productIdDisp)\n    updateDataValue(\"productTypeId\",productTypeIdDisp)\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_PROTECTION_V2 (0x75) : PROTECTION_REPORT_V2 (0x03) )\n *\n *  The Protection Report is used to report the protection state of a device.\n *  I.e. measures to prevent unintentional control (e.g. by a child).\n *\n *  Action: Cache values, update syncPending, and log an info message.\n *\n *  cmd attributes:\n *    Short  localProtectionState  Local protection state (i.e. physical switches/buttons)\n *    Short  rfProtectionState     RF protection state.\n *\n *  Example: ProtectionReport(localProtectionState: 0, reserved01: 0, reserved11: 0, rfProtectionState: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.protectionv2.ProtectionReport cmd) {\n    logger(\"zwaveEvent(): Protection Report received: ${cmd}\",\"trace\")\n\n    def result = []\n\n    state.protectLocalCache = cmd.localProtectionState\n    state.protectRfCache = cmd.rfProtectionState\n\n    def lpStates = [\"unprotected\",\"sequence\",\"noControl\"]\n    def lpValue = lpStates[cmd.localProtectionState]\n    def lpEvent = createEvent(name: \"localProtectionMode\", value: lpValue)\n    if (lpEvent.isStateChange) logger(\"Local Protection set to ${lpValue}.\",\"info\")\n    result << lpEvent\n\n    def rfpStates = [\"unprotected\",\"noControl\",\"noResponse\"]\n    def rfpValue = rfpStates[cmd.rfProtectionState]\n    def rfpEvent = createEvent(name: \"rfProtectionMode\", value: rfpValue)\n    if (rfpEvent.isStateChange) logger(\"RF Protection set to ${rfpValue}.\",\"info\")\n    result << rfpEvent\n\n    logger(\"Protection Report: Local Protection: ${lpValue}, RF Protection: ${rfpValue}\",\"info\")\n    updateSyncPending()\n\n    return result\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_ASSOCIATION_V2 (0x85) : ASSOCIATION_REPORT_V2 (0x03) )\n *\n *  The Association Report command is used to advertise the current destination nodes of a given association group.\n *\n *  Action: Cache value and log info message only.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  Example: AssociationReport(groupingIdentifier: 1, maxNodesSupported: 1, nodeId: [1], reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {\n    logger(\"zwaveEvent(): Association Report received: ${cmd}\",\"trace\")\n\n    state.\"assocGroupCache${cmd.groupingIdentifier}\" = cmd.nodeId\n\n    // Display to user in hex format (same as IDE):\n    def hexArray  = []\n    cmd.nodeId.each { hexArray.add(String.format(\"%02X\", it)) };\n    def assocGroupMd = getAssocGroupsMd().find( { it.id == cmd.groupingIdentifier })\n    logger(\"Association Group ${cmd.groupingIdentifier} [${assocGroupMd?.name}] contains nodes: ${hexArray} (hexadecimal format)\",\"info\")\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_VERSION (0x86) : VERSION_REPORT (0x12) )\n *\n *  The Version Report Command is used to advertise the library type, protocol version, and application version.\n *\n *  Action: Publish values as device 'data' and log an info message.\n *          Store fwVersion as state.fwVersion.\n *\n *  cmd attributes:\n *    Short  applicationSubVersion\n *    Short  applicationVersion\n *    Short  zWaveLibraryType\n *    Short  zWaveProtocolSubVersion\n *    Short  zWaveProtocolVersion\n *\n *  Example: VersionReport(applicationSubVersion: 4, applicationVersion: 3, zWaveLibraryType: 3,\n *   zWaveProtocolSubVersion: 5, zWaveProtocolVersion: 4)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {\n    logger(\"zwaveEvent(): Version Report received: ${cmd}\",\"trace\")\n\n    def zWaveLibraryTypeDisp  = String.format(\"%02X\",cmd.zWaveLibraryType)\n    def zWaveLibraryTypeDesc  = \"\"\n    switch(cmd.zWaveLibraryType) {\n        case 1:\n            zWaveLibraryTypeDesc = \"Static Controller\"\n            break\n\n        case 2:\n            zWaveLibraryTypeDesc = \"Controller\"\n            break\n\n        case 3:\n            zWaveLibraryTypeDesc = \"Enhanced Slave\"\n            break\n\n        case 4:\n            zWaveLibraryTypeDesc = \"Slave\"\n            break\n\n        case 5:\n            zWaveLibraryTypeDesc = \"Installer\"\n            break\n\n        case 6:\n            zWaveLibraryTypeDesc = \"Routing Slave\"\n            break\n\n        case 7:\n            zWaveLibraryTypeDesc = \"Bridge Controller\"\n            break\n\n        case 8:\n            zWaveLibraryTypeDesc = \"Device Under Test (DUT)\"\n            break\n\n        case 0x0A:\n            zWaveLibraryTypeDesc = \"AV Remote\"\n            break\n\n        case 0x0B:\n            zWaveLibraryTypeDesc = \"AV Device\"\n            break\n\n        default:\n            zWaveLibraryTypeDesc = \"N/A\"\n    }\n\n    def applicationVersionDisp = String.format(\"%d.%02d\",cmd.applicationVersion,cmd.applicationSubVersion)\n    def zWaveProtocolVersionDisp = String.format(\"%d.%02d\",cmd.zWaveProtocolVersion,cmd.zWaveProtocolSubVersion)\n\n    state.fwVersion = new BigDecimal(applicationVersionDisp)\n\n    logger(\"Version Report: Application Version: ${applicationVersionDisp}, \" +\n           \"Z-Wave Protocol Version: ${zWaveProtocolVersionDisp}, \" +\n           \"Z-Wave Library Type: ${zWaveLibraryTypeDisp} (${zWaveLibraryTypeDesc})\",\"info\")\n\n    updateDataValue(\"applicationVersion\",\"${cmd.applicationVersion}\")\n    updateDataValue(\"applicationSubVersion\",\"${cmd.applicationSubVersion}\")\n    updateDataValue(\"zWaveLibraryType\",\"${zWaveLibraryTypeDisp}\")\n    updateDataValue(\"zWaveProtocolVersion\",\"${cmd.zWaveProtocolVersion}\")\n    updateDataValue(\"zWaveProtocolSubVersion\",\"${cmd.zWaveProtocolSubVersion}\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_INDICATOR (0x87) : INDICATOR_REPORT (0x03) )\n *\n *  The Indicator Report command is used to advertise the state of an indicator.\n *\n *  Action: Do nothing. It doesn't tell us anything useful.\n *\n *  cmd attributes:\n *    Short value  Indicator status.\n *      0x00       = Off/Disabled\n *      0x01..0x63 = Indicator Range.\n *      0xFF       = On/Enabled.\n *\n *  Example: IndicatorReport(value: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.indicatorv1.IndicatorReport cmd) {\n    logger(\"zwaveEvent(): Indicator Report received: ${cmd}\",\"trace\")\n}\n\n/**\n *  zwaveEvent( DEFAULT CATCHALL )\n *\n *  Called for all commands that aren't handled above.\n **/\ndef zwaveEvent(physicalgraph.zwave.Command cmd) {\n    logger(\"zwaveEvent(): No handler for command: ${cmd}\",\"error\")\n}\n\n/*****************************************************************************************************************\n *  Capability-related Commands:\n *****************************************************************************************************************/\n\n/**\n *  on()                        [Capability: Switch]\n *\n *  Turn the switch on.\n **/\ndef on() {\n    logger(\"on(): Turning switch on.\",\"info\")\n        sendCommands([\n        zwave.basicV1.basicSet(value: 0xFF).format(),\n        zwave.switchBinaryV1.switchBinaryGet().format(),\n        \"delay 3000\",\n        zwave.meterV2.meterGet(scale: 2).format()\n    ])\n}\n\n/**\n *  off()                       [Capability: Switch]\n *\n *  Turn the switch off.\n **/\ndef off() {\n    logger(\"off(): Turning switch off.\",\"info\")\n    sendCommands([\n        zwave.basicV1.basicSet(value: 0x00).format(),\n        zwave.switchBinaryV1.switchBinaryGet().format(),\n        \"delay 3000\",\n        zwave.meterV2.meterGet(scale: 2).format()\n    ])\n}\n\n/**\n *  poll()                      [Capability: Polling]\n *\n *  Calls refresh().\n **/\ndef poll() {\n    logger(\"poll()\",\"trace\")\n    refresh()\n}\n\n/**\n *  refresh()                   [Capability: Refresh]\n *\n *  Action: Request switchBinary, energy, and power reports. Plus, get wheel status.\n *  Trigger a sync too.\n **/\ndef refresh() {\n    logger(\"refresh()\",\"trace\")\n    sendCommands([\n        zwave.switchBinaryV1.switchBinaryGet().format(),\n        zwave.meterV2.meterGet(scale: 0).format(),\n        zwave.meterV2.meterGet(scale: 2).format(),\n        zwave.configurationV1.configurationGet(parameterNumber: 2) // Wheel Status\n    ])\n    sync()\n}\n\n/*****************************************************************************************************************\n *  Custom Commands:\n *****************************************************************************************************************/\n\n/**\n *  blink()\n *\n *  Causes the Circle LED to blink for ~20 seconds.\n **/\ndef blink() {\n    logger(\"blink(): Blinking Circle LED\",\"info\")\n    sendCommands([zwave.indicatorV1.indicatorSet(value: 255)])\n}\n\n/**\n *  autoOff()\n *\n *  Calls off(), but with additional log message.\n **/\ndef autoOff() {\n    logger(\"autoOff(): Automatically turning off the device.\",\"info\")\n    off()\n}\n\n/**\n *  reset()\n *\n *  Alias for resetEnergy().\n *\n *  Note: this used to be part of the official 'Energy Meter' capability, but isn't anymore.\n **/\ndef reset() {\n    logger(\"reset()\",\"trace\")\n    resetEnergy()\n}\n\n/**\n *  resetEnergy()\n *\n *  Reset the Accumulated Energy figure held in the device.\n **/\ndef resetEnergy() {\n    logger(\"resetEnergy(): Resetting Accumulated Energy\",\"info\")\n\n    state.energyLastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"energyLastReset\", value: state.energyLastReset, descriptionText: \"Accumulated Energy Reset\")\n\n    sendCommands([\n        zwave.meterV3.meterReset(),\n        zwave.meterV3.meterGet(scale: 0)\n    ],400)\n}\n\n/**\n *  resetFault()\n *\n *  Reset fault alarm to 'clear'.\n **/\ndef resetFault() {\n    logger(\"resetFault(): Resetting fault alarm.\",\"info\")\n    sendEvent(name: \"fault\", value: \"clear\", descriptionText: \"Fault alarm cleared\", displayed: true)\n}\n\n/**\n *  setLocalProtectionMode(localProtectionMode)\n *\n *  Set local (physical) protection mode.\n *\n *  Note: GreenWave PowerNode supports \"unprotected\" and \"noControl\" modes only.\n *\n *  localProtectionMode values:\n *   \"unprotected\"  Physical switches are operational.\n *   \"sequence\"     Special sequence required to operate.\n *   \"noControl\"    Physical switches are disabled.\n **/\ndef setLocalProtectionMode(localProtectionMode) {\n    logger(\"setLocalProtectionMode(${localProtectionMode})\",\"trace\")\n\n    switch(localProtectionMode.toLowerCase()) {\n        case \"unprotected\":\n            state.protectLocalTarget = 0\n            break\n        case \"sequence\":\n            logger(\"setLocalProtectionMode(): Protection by sequence is not supported by this device.\",\"warn\")\n            state.protectLocalTarget = 2\n            break\n        case \"nocontrol\":\n            state.protectLocalTarget = 2\n            break\n        default:\n            logger(\"setLocalProtectionMode(): Unknown protection mode: ${localProtectionMode}.\",\"warn\")\n    }\n    sync()\n}\n\n/**\n *  toggleLocalProtectionMode()\n *\n *  Toggle local (physical) protection mode between \"unprotected\" and \"noControl\" modes.\n **/\ndef toggleLocalProtectionMode() {\n    logger(\"toggleLocalProtectionMode()\",\"trace\")\n\n    if (device.latestValue(\"localProtectionMode\") != \"unprotected\") {\n        setLocalProtectionMode(\"unprotected\")\n    }\n    else {\n        setLocalProtectionMode(\"noControl\")\n    }\n}\n\n/**\n *  setRfProtectionMode(rfProtectionMode)\n *\n *  Set RF (wireless) protection mode.\n *\n *  Note: GreenWave PowerNode supports \"unprotected\" and \"noControl\" modes only.\n *\n *  rfProtectionMode values:\n *   \"unprotected\"   Device responds to wireless commands.\n *   \"noControl\"     Device ignores wireless commands (sends ApplicationRejectedRequest).\n *   \"noResponse\"    Device ignores wireless commands.\n **/\ndef setRfProtectionMode(rfProtectionMode) {\n    logger(\"setRfProtectionMode(${rfProtectionMode})\",\"trace\")\n\n    switch(rfProtectionMode.toLowerCase()) {\n        case \"unprotected\":\n            state.protectRfTarget = 0\n            break\n        case \"nocontrol\":\n            state.protectRfTarget = 1\n            break\n        case \"noresponse\":\n            logger(\"setRfProtectionMode(): NoResponse mode is not supported by this device.\",\"warn\")\n            state.protectRfTarget = 1\n            break\n        default:\n            logger(\"setRfProtectionMode(): Unknown protection mode: ${rfProtectionMode}.\",\"warn\")\n    }\n    sync()\n}\n\n/**\n *  toggleRfProtectionMode()\n *\n *  Toggle RF (wireless) protection mode between \"unprotected\" and \"noControl\" modes.\n **/\ndef toggleRfProtectionMode() {\n    logger(\"toggleRfProtectionMode()\",\"trace\")\n\n    if (device.latestValue(\"rfProtectionMode\") != \"unprotected\") {\n        setRfProtectionMode(\"unprotected\")\n    }\n    else {\n        setRfProtectionMode(\"noControl\")\n    }\n}\n\n\n/*****************************************************************************************************************\n *  Private Helper Functions:\n *****************************************************************************************************************/\n\n/**\n *  encapCommand(cmd)\n *\n *  Applies security or CRC16 encapsulation to a command as needed.\n *  Returns a physicalgraph.zwave.Command.\n **/\nprivate encapCommand(physicalgraph.zwave.Command cmd) {\n    if (state.useSecurity) {\n        return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd)\n    }\n    else if (state.useCrc16) {\n        return zwave.crc16EncapV1.crc16Encap().encapsulate(cmd)\n    }\n    else {\n        return cmd\n    }\n}\n\n/**\n *  prepCommands(cmds, delay=200)\n *\n *  Converts a list of commands (and delays) into a HubMultiAction object, suitable for returning via parse().\n *  Uses encapCommand() to apply security or CRC16 encapsulation as needed.\n **/\nprivate prepCommands(cmds, delay=200) {\n    return response(delayBetween(cmds.collect{ (it instanceof physicalgraph.zwave.Command ) ? encapCommand(it).format() : it },delay))\n}\n\n/**\n *  sendCommands(cmds, delay=200)\n *\n *  Sends a list of commands directly to the device using sendHubCommand.\n *  Uses encapCommand() to apply security or CRC16 encapsulation as needed.\n **/\nprivate sendCommands(cmds, delay=200) {\n    sendHubCommand( cmds.collect{ (it instanceof physicalgraph.zwave.Command ) ? response(encapCommand(it)) : response(it) }, delay)\n}\n\n/**\n *  logger()\n *\n *  Wrapper function for all logging:\n *    Logs messages to the IDE (Live Logging), and also keeps a historical log of critical error and warning\n *    messages by sending events for the device's logMessage attribute.\n *    Configured using configLoggingLevelIDE and configLoggingLevelDevice preferences.\n **/\nprivate logger(msg, level = \"debug\") {\n\n    switch(level) {\n        case \"error\":\n            if (state.loggingLevelIDE >= 1) log.error msg\n            if (state.loggingLevelDevice >= 1) sendEvent(name: \"logMessage\", value: \"ERROR: ${msg}\", displayed: false, isStateChange: true)\n            break\n\n        case \"warn\":\n            if (state.loggingLevelIDE >= 2) log.warn msg\n            if (state.loggingLevelDevice >= 2) sendEvent(name: \"logMessage\", value: \"WARNING: ${msg}\", displayed: false, isStateChange: true)\n            break\n\n        case \"info\":\n            if (state.loggingLevelIDE >= 3) log.info msg\n            break\n\n        case \"debug\":\n            if (state.loggingLevelIDE >= 4) log.debug msg\n            break\n\n        case \"trace\":\n            if (state.loggingLevelIDE >= 5) log.trace msg\n            break\n\n        default:\n            log.debug msg\n            break\n    }\n}\n\n/**\n *  sync()\n *\n *  Manages synchronisation of parameters, association groups, etc. with the physical device.\n *  The syncPending attribute advertises remaining number of sync operations.\n *\n *  Does not return a list of commands, it sends them immediately using sendCommands(), which means sync() can be\n *  triggered by schedule().\n *\n *  Parameters:\n *   forceAll    Force all items to be synced, otherwise only changed items will be synced.\n **/\nprivate sync(forceAll = false) {\n    logger(\"sync(): Syncing configuration with the physical device.\",\"info\")\n\n    def cmds = []\n    def syncPending = 0\n\n    if (forceAll) { // Clear all cached values.\n        getParamsMd().findAll( {!it.readonly} ).each { state.\"paramCache${it.id}\" = null }\n        getAssocGroupsMd().each { state.\"assocGroupCache${it.id}\" = null }\n        state.protectLocalCache = null\n        state.protectRfCache = null\n        state.switchAllModeCache = null\n    }\n\n    getParamsMd().findAll( { !it.readonly & (it.fwVersion <= state.fwVersion) } ).each { // Exclude readonly/newer parameters.\n        if ( (state.\"paramTarget${it.id}\" != null) & (state.\"paramCache${it.id}\" != state.\"paramTarget${it.id}\") ) {\n            cmds << zwave.configurationV1.configurationSet(parameterNumber: it.id, size: it.size, scaledConfigurationValue: state.\"paramTarget${it.id}\".toInteger())\n            cmds << zwave.configurationV1.configurationGet(parameterNumber: it.id)\n            logger(\"sync(): Syncing parameter #${it.id} [${it.name}]: New Value: \" + state.\"paramTarget${it.id}\",\"info\")\n            syncPending++\n            }\n    }\n\n    getAssocGroupsMd().each {\n        def cachedNodes = state.\"assocGroupCache${it.id}\"\n        def targetNodes = state.\"assocGroupTarget${it.id}\"\n\n        if ( cachedNodes != targetNodes ) {\n            // Display to user in hex format (same as IDE):\n            def targetNodesHex  = []\n            targetNodes.each { targetNodesHex.add(String.format(\"%02X\", it)) }\n            logger(\"sync(): Syncing Association Group #${it.id} [${it.name}]: Destinations: ${targetNodesHex}\",\"info\")\n            if (it.multiChannel) {\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: it.id, nodeId: []) // Remove All\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: it.id, nodeId: targetNodes)\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: it.id)\n            }\n            else {\n                cmds << zwave.associationV2.associationRemove(groupingIdentifier: it.id, nodeId: []) // Remove All\n                cmds << zwave.associationV2.associationSet(groupingIdentifier: it.id, nodeId:[zwaveHubNodeId])\n                cmds << zwave.associationV2.associationGet(groupingIdentifier: it.id)\n            }\n            syncPending++\n        }\n    }\n\n    if ( (state.protectLocalTarget != null) & (state.protectRfTarget != null)\n      & ( (state.protectLocalCache != state.protectLocalTarget) || (state.protectRfCache != state.protectRfTarget) ) ) {\n\n        logger(\"sync(): Syncing Protection State: Local Protection: ${state.protectLocalTarget}, RF Protection: ${state.protectRfTarget}\",\"info\")\n        cmds << zwave.protectionV2.protectionSet(localProtectionState : state.protectLocalTarget, rfProtectionState: state.protectRfTarget)\n        cmds << zwave.protectionV2.protectionGet()\n        syncPending++\n    }\n\n    if ( (state.switchAllModeTarget != null) & (state.switchAllModeCache != state.switchAllModeTarget) ) {\n        logger(\"sync(): Syncing SwitchAll Mode: ${state.switchAllModeTarget}\",\"info\")\n        cmds << zwave.switchAllV1.switchAllSet(mode: state.switchAllModeTarget)\n        cmds << zwave.switchAllV1.switchAllGet()\n        syncPending++\n    }\n\n    sendEvent(name: \"syncPending\", value: syncPending, displayed: false)\n    sendCommands(cmds,800)\n}\n\n/**\n *  updateSyncPending()\n *\n *  Updates syncPending attribute, which advertises remaining number of sync operations.\n **/\nprivate updateSyncPending() {\n\n    def syncPending = 0\n\n    getParamsMd().findAll( { !it.readonly & (it.fwVersion <= state.fwVersion) } ).each { // Exclude readonly/newer parameters.\n        if ( (state.\"paramTarget${it.id}\" != null) & (state.\"paramCache${it.id}\" != state.\"paramTarget${it.id}\") ) {\n            syncPending++\n        }\n    }\n\n    getAssocGroupsMd().each {\n        def cachedNodes = state.\"assocGroupCache${it.id}\"\n        def targetNodes = state.\"assocGroupTarget${it.id}\"\n\n        if ( cachedNodes != targetNodes ) {\n            syncPending++\n        }\n    }\n\n    if ( (state.protectLocalCache == null) || (state.protectRfCache == null) ||\n         (state.protectLocalCache != state.protectLocalTarget) || (state.protectRfCache != state.protectRfTarget) ) {\n        syncPending++\n    }\n\n    if ( (state.switchAllModeTarget != null) & (state.switchAllModeCache != state.switchAllModeTarget) ) {\n        syncPending++\n    }\n\n    logger(\"updateSyncPending(): syncPending: ${syncPending}\", \"debug\")\n    if ((syncPending == 0) & (device.latestValue(\"syncPending\") > 0)) logger(\"Sync Complete.\", \"info\")\n    sendEvent(name: \"syncPending\", value: syncPending, displayed: false)\n}\n\n/**\n *  generatePrefsParams()\n *\n *  Generates preferences (settings) for device parameters.\n **/\nprivate generatePrefsParams() {\n        section {\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"DEVICE PARAMETERS:\",\n                description: \"Device parameters are used to customise the physical device. \" +\n                             \"Refer to the product documentation for a full description of each parameter.\"\n            )\n\n    getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.\n\n        def lb = (it.description.length() > 0) ? \"\\n\" : \"\"\n\n        switch(it.type) {\n            case \"number\":\n            input (\n                name: \"configParam${it.id}\",\n                title: \"#${it.id}: ${it.name}: \\n\" + it.description + lb +\"Default Value: ${it.defaultValue}\",\n                type: it.type,\n                range: it.range,\n//                defaultValue: it.defaultValue, // iPhone users can uncomment these lines!\n                required: it.required\n            )\n            break\n\n            case \"enum\":\n            input (\n                name: \"configParam${it.id}\",\n                title: \"#${it.id}: ${it.name}: \\n\" + it.description + lb + \"Default Value: ${it.defaultValue}\",\n                type: it.type,\n                options: it.options,\n//                defaultValue: it.defaultValue, // iPhone users can uncomment these lines!\n                required: it.required\n            )\n            break\n        }\n    }\n        } // section\n}\n\n/**\n *  generatePrefsAssocGroups()\n *\n *  Generates preferences (settings) for Association Groups.\n *  Excludes any groups that are hubOnly.\n **/\nprivate generatePrefsAssocGroups() {\n        section {\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"ASSOCIATION GROUPS:\",\n                description: \"Association groups enable the device to control other Z-Wave devices directly, \" +\n                             \"without participation of the main controller.\\n\" +\n                             \"Enter a comma-delimited list of destinations (node IDs and/or endpoint IDs) for \" +\n                             \"each association group. All IDs must be in hexadecimal format. E.g.:\\n\" +\n                             \"Node destinations: '11, 0F'\\n\" +\n                             \"Endpoint destinations: '1C:1, 1C:2'\"\n            )\n\n    getAssocGroupsMd().findAll( { !it.hubOnly } ).each {\n            input (\n                name: \"configAssocGroup${it.id}\",\n                title: \"Association Group #${it.id}: ${it.name}: \\n\" + it.description + \" \\n[MAX NODES: ${it.maxNodes}]\",\n                type: \"text\",\n//                defaultValue: \"\", // iPhone users can uncomment these lines!\n                required: false\n            )\n        }\n    }\n}\n\n/**\n *  byteArrayToUInt(byteArray)\n *\n *  Converts a byte array to an UNSIGNED int.\n **/\nprivate byteArrayToUInt(byteArray) {\n    // return java.nio.ByteBuffer.wrap(byteArray as byte[]).getInt()\n    def i = 0\n    byteArray.reverse().eachWithIndex { b, ix -> i += b * (0x100 ** ix) }\n    return i\n}\n\n/**\n *  test()\n *\n *  Called from 'test' tile.\n **/\nprivate test() {\n    logger(\"test()\",\"trace\")\n\n    def cmds = []\n\n    sendCommands(cmds, 500)\n}\n\n/*****************************************************************************************************************\n *  Static Matadata Functions:\n *\n *  These functions encapsulate metadata about the device. Mostly obtained from:\n *   Z-wave Alliance Reference: http://products.z-wavealliance.org/products/1036\n *****************************************************************************************************************/\n\n/**\n *  getCommandClassVersions()\n *\n *  Returns a map of the command class versions supported by the device. Used by parse() and zwaveEvent() to\n *  extract encapsulated commands from MultiChannelCmdEncap, MultiInstanceCmdEncap, SecurityMessageEncapsulation,\n *  and Crc16Encap messages.\n *\n *  Reference: http://products.z-wavealliance.org/products/629/classes\n **/\nprivate getCommandClassVersions() {\n    return [\n        0x20: 1, // Basic V1\n        0x22: 1, // Application Status V1 (Not advertised but still sent)\n        0x25: 1, // Switch Binary V1\n        0x27: 1, // Switch All V1\n        0x32: 3, // Meter V3\n        0x56: 1, // CRC16 Encapsulation V1\n        0x70: 1, // Configuration V1\n        0x71: 1, // Alarm (Notification) V1\n        0x72: 2, // Manufacturer Specific V2\n        0x75: 2, // Protection V2\n        0x85: 2, // Association V2\n        0x86: 1, // Version V1\n        0x87: 1 // Indicator V1\n    ]\n}\n\n/**\n *  getParamsMd()\n *\n *  Returns device parameters metadata. Used by sync(), updateSyncPending(), and generatePrefsParams().\n *\n *  List attributes:\n *   id/size/type/range/defaultValue/required/name/description/options    These directly correspond to input attributes.\n *   readonly     If the parameter is readonly, then it will not be displayed by generatePrefsParams() or synced.\n *   isSigned     Indicates if the raw byte value represents a signed or unsigned number.\n *   fwVersion    The minimum firmware version that supports the parameter. Parameters with a higher fwVersion than the\n *                device instance will not be displayed by generatePrefsParams() or synced.\n **/\nprivate getParamsMd() {\n    return [\n        // Firmware v4.22 onwards:\n        [id:  0, size: 1, type: \"number\", range: \"1..100\", defaultValue: 10, required: false, readonly: false,\n         isSigned: true, fwVersion: 4.22,\n         name: \"Power Report Threshold\",\n         description : \"Power level change that will result in a new power report being sent.\\n\" +\n         \"Values: 1-100 = % change from previous report\"],\n        [id:  1, size: 1, type: \"number\", range: \"0..255\", defaultValue: 255, required: false, readonly: false, // Real default is 2.\n         isSigned: false, fwVersion: 4.22,\n         name: \"Keep-Alive Time\",\n         description : \"Time after which the LED indicator will flash if there has been no communication from the hub.\\n\" +\n         \"Values: 1-255 = time in minutes\"],\n        [id: 2, size: 1, type: \"number\", defaultValue: 0, required: false, readonly: true, // READ-ONLY!\n         isSigned: false, fwVersion: 4.22,\n         name: \"Wheel Status\",\n         description : \"Indicates the position of the Room Colour Selector wheel.\"],\n        // Firmware v4.28 onwards:\n        [id: 3, size: 1, type: \"enum\", defaultValue: \"2\", required: false, readonly: false,\n         isSigned: true, fwVersion: 4.28,\n         name: \"State After Power Failure\",\n         description : \"Switch state to restore after a power failure. [Firmware 4.28+ Only]\",\n         options: [\"0\" : \"0: Off\",\n                   \"1\" : \"1: Restore Previous State\",\n                   \"2\" : \"2: On\"] ],\n        [id: 4, size: 1, type: \"enum\", defaultValue: \"1\", required: false, readonly: false,\n         isSigned: true, fwVersion: 4.28,\n         name: \"LED for Network Error\",\n         description : \"LED indicates network error. [Firmware 4.28+ Only]\",\n         options: [\"0\" : \"0: DISABLED\",\n                   \"1\" : \"1: ENABLED\"] ]\n    ]\n}\n\n/**\n *  getAssocGroupsMd()\n *\n *  Returns association groups metadata. Used by sync(), updateSyncPending(), and generatePrefsAssocGroups().\n *\n *  List attributes:\n *   id            Association group ID (groupingIdentifier).\n *   maxNodes      Maximum nodes supported.\n *   name          Name, shown on device settings screen and logs.\n *   hubOnly       Group should only contain the SmartThings hub (not shown on settings screen).\n *   multiChannel  Group supports multiChannelAssociation.\n *   description   Description, shown on device settings screen.\n **/\nprivate getAssocGroupsMd() {\n    return [\n        [id: 1, maxNodes: 1, name: \"Wheel Status\", hubOnly: true, multiChannel: false,\n         description : \"Reports wheel status using CONFIGURATION_REPORT commands.\"],\n        [id: 2, maxNodes: 1, name: \"Relay Health\", hubOnly: true, multiChannel: false,\n         description : \"Sends ALARM commands when current leakage is detected.\"],\n        [id: 3, maxNodes: 1, name: \"Power Level\", hubOnly: true, multiChannel: false,\n         description : \"Reports instantaneous power using METER_REPORT commands (configured using parameter #0).\"],\n        [id: 4, maxNodes: 1, name: \"Overcurrent Protection\", hubOnly: true, multiChannel: false,\n         description : \"Sends ALARM commands when overcurrent is detected.\"]\n    ]\n}\n\n/**\n *  getWheelColours()\n *\n *  Returns a map of wheel colours.\n **/\nprivate getWheelColours() {\n    return [\n        0x80 : \"black\",\n        0x81 : \"green\",\n        0x82 : \"blue\",\n        0x83 : \"red\",\n        0x84 : \"yellow\",\n        0x85 : \"violet\",\n        0x86 : \"orange\",\n        0x87 : \"aqua\",\n        0x88 : \"pink\",\n        0x89 : \"white\"\n    ]\n}"
  },
  {
    "path": "devices/philio-dual-relay/philio-dual-relay.groovy",
    "content": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: Philio Dual Relay (PAN04) Single Mode\n *\n *  Author: David Lomas (codersaur)\n *\n *  Date: 2016-03-01\n *\n *  Version: 1.00\n *\n *  Description:\n *   - This device handler is written specifically for the Philio Dual Relay (PAN04), when used as a single switch/relay only.\n *     (ON/OFF will turn both relays ON/OFF. METER reports are combined total of relay 1 & 2).\n *      Hence, this device handler does not issue or parse any MULTI_CHANNEL_V3 events.\n *   - Supports live reporting of energy, power, current, voltage, and powerFactor. Press the 'Now' tile to refresh.\n *      (voltage and powerFactor tiles are not shown by default, but you can enable them below).\n *   - Supports reporting of energy usage and cost over an ad hoc period, based on the 'energy' figure reported by \n *     the device. Press the 'Since...' tile to reset.\n *   - Supports additional reporting of energy usage and cost over multiple pre-defined periods:\n *       'Today', 'Last 24 Hours', 'Last 7 Days', 'This Month', 'This Year', and 'Lifetime'\n *     These can be cycled through by pressing the 'statsMode' tile. There's also a tile that will reset all Energy\n *     Stats periods, but it's hidden by default.\n *   - All configurable device parameters can be set from the device settings. Refer to the PAN04 instruction \n *     manual for full details.\n *   - If you are re-using this device, please use your own hosting for the icons.\n *\n *  PAN04 device notes:\n *   - Supported Command Classes:\n *      COMMAND_CLASS_BASIC [0x20: 1]\n *      COMMAND_CLASS_SWITCH_BINARY [0x25: 1]\n *      COMMAND_CLASS_SWITCH_ALL [0x27: 1]\n *      COMMAND_CLASS_METER_V3 [0x32: 3]\n *      COMMAND_CLASS_MULTI_CHANNEL_V3 [0x60: 3]\n *      COMMAND_CLASS_CONFIGURATION [0x70: 1]\n *      COMMAND_CLASS_ALARM [0x71: 1]\n *      COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 [0x72: 2]\n *      COMMAND_CLASS_ASSOCIATION_V1 [0x85: 1]\n *      COMMAND_CLASS_VERSION [0x86: 1]\n *   - Association Groups receive auto-reports for switch, energy, and power:\n *      Association Group #1 will receive BINARY and METER auto-reports for Relay 1 & 2.\n *      Association Group #2 will receive BINARY and METER auto-reports for Relay 1 only.\n *      Association Group #3 will receive BINARY and METER auto-reports for Relay 2 only.\n *   - The PAN04 cannot be configured to send auto-reports for voltage, current, or powerFactor. \n *     Therefore, meter reports for current and powerFactor are requested whenever a meter report for power is received.\n *     Additionally, a meter report for voltage is reqeusted whenever a meter report for energy is received.\n *\n *  Version History:\n *\n *   2016-03-01: v1.0\n *    - Initial Version for Philio Dual Relay (PAN04) in Single Switch Mode.\n * \n *  To Do:\n *   - Option to specify a '£/day' fixed charge, which is added to all energy cost calculations.\n *   - Process Alarm reports.\n *   - Add Min/Max/Ave stats (instMode tile to cycle through: Now/Min/Max/Ave).\n *   - Additional Device Handler for full dual relay mode.\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n **/\n\nmetadata {\n\tdefinition (name: \"Philio Dual Relay (PAN04) Single Mode\", namespace: \"codersaur\", author: \"David Lomas\") {\n\t\tcapability \"Actuator\"\n\t\tcapability \"Switch\"\n\t\tcapability \"Power Meter\"\n\t\tcapability \"Energy Meter\"\n\t\t//capability \"Voltage Measurement\"  // In documentation, but generates RunTimeException.\n\t\tcapability \"Polling\"\n\t\tcapability \"Refresh\"\n\t\tcapability \"Configuration\"\n\t\tcapability \"Sensor\"\n\n\t\tcommand \"reset\"\n        command \"refresh\"\n        command \"configure\"\n        command \"updated\"\n        command \"poll\"\n        command \"cycleStats\"\n\t\tcommand \"resetAllStats\"\n\t\tcommand \"test\"\n        \n\t\t// Standard (Capability) Attributes:\n\t\tattribute \"switch\", \"string\"\n        attribute \"power\", \"number\"\n        attribute \"energy\", \"number\" // Energy (kWh) as reported by device (ad hoc period).\n        \n        // Custom Attributes:\n        attribute \"current\", \"number\"\n        attribute \"voltage\", \"number\"\n        attribute \"powerFactor\", \"number\"\n\t\tattribute \"lastReset\", \"string\" // Time that ad hoc reporting was reset.\n\t\tattribute \"statsMode\", \"string\"\n\t\tattribute \"costOfEnergy\", \"number\" \n\t\tattribute \"energyToday\", \"number\"\n\t\tattribute \"costOfEnergyToday\", \"number\"\n\t\tattribute \"energy24Hours\", \"number\"\n\t\tattribute \"costOfEnergy24Hours\", \"number\"\n\t\tattribute \"energy7Days\", \"number\"\n\t\tattribute \"costOfEnergy7Days\", \"number\"\n\t\tattribute \"energyMonth\", \"number\"\n\t\tattribute \"costOfEnergyMonth\", \"number\"\n\t\tattribute \"energyYear\", \"number\"\n\t\tattribute \"costOfEnergyYear\", \"number\"\n\t\tattribute \"energyLifetime\", \"number\"\n\t\tattribute \"costOfEnergyLifetime\", \"number\"\n        attribute \"secondaryInfo\", \"string\"\n        \n        // Display Attributes:\n        // These are only required because the UI lacks number formatting and strips leading zeros.\n        attribute \"dispPower\", \"string\"\n        attribute \"dispCurrent\", \"string\"\n        attribute \"dispVoltage\", \"string\"\n        attribute \"dispPowerFactor\", \"string\"\n        attribute \"dispEnergy\", \"string\"\n        attribute \"dispCostOfEnergy\", \"string\"\n        attribute \"dispEnergyPeriod\", \"string\"\n        attribute \"dispCostOfEnergyPeriod\", \"string\"\n        \n        // Fingerprints:\n\t\tfingerprint deviceId:\"0x1001\", inClusters:\"0x20 0x25 0x27 0x72 0x86 0x32 0x60 0x85 0x70 0x71\"\n\t}\n\n\t// Tile definitions:\n\ttiles(scale: 2) {\n    \n\t\t// Main Tiles:\n        standardTile(\"switch\", \"device.switch\", width: 2, height: 2, decoration: \"flat\", canChangeIcon: true) {\n\t\t\tstate \"on\", label: '${name}', action: \"switch.off\", icon: \"st.switches.switch.on\", backgroundColor: \"#79b821\"\n\t\t\tstate \"off\", label: '${name}', action: \"switch.on\", icon: \"st.switches.switch.off\", backgroundColor: \"#ffffff\"\n\t\t}\n        \n        // Multi Tile:\n\t\tmultiAttributeTile(name:\"multi1\", type: \"generic\", width: 4, height: 4, canChangeIcon: true) {\n\t\t\ttileAttribute (\"device.switch\", key: \"PRIMARY_CONTROL\") {\n\t\t\t\tattributeState \"on\", label: '${name}', action: \"switch.off\", icon: \"st.switches.switch.on\", backgroundColor: \"#79b821\"\n\t\t\t\tattributeState \"off\", label: '${name}', action: \"switch.on\", icon: \"st.switches.switch.off\", backgroundColor: \"#ffffff\"\n\t\t\t}\n\t\t\ttileAttribute (\"device.secondaryInfo\", key: \"SECONDARY_CONTROL\") {\n\t\t\t\tattributeState \"default\", label:'${currentValue}'\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Instantaneous Values:\n\t\tvalueTile(\"instMode\", \"device.dispPower\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Now:', action:\"refresh.refresh\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_refresh.png\"\n\t\t}\n\t\tvalueTile(\"power\", \"device.dispPower\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"current\", \"device.dispCurrent\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"voltage\", \"device.dispVoltage\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"powerFactor\", \"device.dispPowerFactor\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t\n        // Ad Hoc Energy Stats:\n\t\tvalueTile(\"lastReset\", \"device.lastReset\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Since:  ${currentValue}', action:\"reset\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_stopwatch_reset.png\"\n\t\t}\n\t\tvalueTile(\"energy\", \"device.dispEnergy\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costOfEnergy\", \"device.dispCostOfEnergy\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t\n\t\t// Energy Stats:\n        // Needs to be a standardTile to be able to change icon for each state.\n\t\tvalueTile(\"statsMode\", \"device.statsMode\", decoration: \"flat\", canChangeIcon: true, canChangeBackground: true, width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"Today\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"Last 24 Hours\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"Last 7 Days\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"This Month\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"This Year\", label:\"${currentValue}:\", action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"Lifetime\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t}\n\t\tvalueTile(\"energyPeriod\", \"device.dispEnergyPeriod\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costOfEnergyPeriod\", \"device.dispCostOfEnergyPeriod\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costPerKWH\", \"device.costPerKWH\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Unit Cost: ${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t\n\t\t// Action Buttons:\n\t\tstandardTile(\"refresh\", \"device.power\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'', action:\"refresh.refresh\", icon:\"st.secondary.refresh\"\n\t\t}\n\t\tstandardTile(\"resetAllStats\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'RESET ALL STATS!', action:\"resetAllStats\"\n\t\t}\n\t\tstandardTile(\"configure\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'', action:\"configuration.configure\", icon:\"st.secondary.configure\"\n\t\t}\n\t\tstandardTile(\"test\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'Test', action:\"test\"\n\t\t}\n\t\t\n\t\t// Tile layouts:\n\t\tmain([\"switch\",\"power\",\"energy\"])\n\t\tdetails([\n\t\t\t// Multi Tile:\n\t\t\t\"multi1\"\n\t\t\t// Instantaneous Values:\n\t\t\t,\"instMode\",\"power\", \"current\" //,\"voltage\", \"powerFactor\"\n\t\t\t// Ad Hoc Stats:\n\t\t\t,\"lastReset\", \"energy\", \"costOfEnergy\"\t\n\t\t\t// Energy Stats:\n\t\t\t,\"statsMode\", \"energyPeriod\", \"costOfEnergyPeriod\" //,\"costPerKWH\"\n\t\t\t// Action Buttons:\n\t\t\t//, \"refresh\",\"resetAllStats\",\"configure\",\"test\"\n\t\t])\n\t}\n    \n    preferences {\n    \t\n        input \"configCostPerKWH\", \"string\", title: \"Energy Cost (£/kWh)\", defaultValue: \"0.1253\", required: true, displayDuringSetup: true\n    \tinput \"configAutoReport\", \"boolean\", title: \"Enable Auto-Reporting?\", defaultValue: true, required: false, displayDuringSetup: true\n\n\t\t// Device Configuration Parameters (see PAN04 instruction manual):\n    \tinput \"configParameter1\", \"number\", title: \"Power Report Interval (x5sec):\", defaultValue: 12, required: false, displayDuringSetup: true // 1 min.\n    \tinput \"configParameter2\", \"number\", title: \"Energy Report Interval (x10min):\", defaultValue: 1, required: false, displayDuringSetup: true // 10 min.\n\t\t// Parameter #3 is disbaled in this device handler, as it should always be \"Relay 1 & 2\" for single switch mode.\n\t\t//input \"configParameter3\", \"enum\", title: \"Selected End Point For Basic Commands:\", \n\t\t//\toptions:[\"Relay 1 & 2\", \"Relay 1\", \"Relay 2\"], defaultValue: \"Relay 1 & 2\", required: false, displayDuringSetup: true\n\t\tinput \"configParameter4\", \"enum\", title: \"Manual Switch Mode:\", \n\t\t\toptions:[\"Edge\", \"Pulse\", \"Edge-Toggle\"], defaultValue: \"Edge\", required: false, displayDuringSetup: true\n        input \"configParameter5\", \"number\", title: \"Power Threshold for Load Caution (W):\", defaultValue: 1500, required: false, displayDuringSetup: true\n        input \"configParameter6\", \"number\", title: \"Energy Threshold for Load Caution (kWh):\", defaultValue: 10000, required: false, displayDuringSetup: true\n        \t\n\t\t// Debug Mode:\n\t\tinput \"configDebugMode\", \"boolean\", title: \"Enable debug logging?\", defaultValue: true, required: false, displayDuringSetup: true\n    }\n}\n\n\n/**********************************************************************\n *  Z-wave Event Handlers.\n **********************************************************************/\n\n/**\n *  parse() - Called when messages from a device are received by the hub.\n *\n *  The parse method is responsible for interpreting those messages and returning Event definitions.\n *\n *  String \t\tdescription \t\t- The message from the device.\n **/\ndef parse(String description) {\n\tif (state.debug) log.debug \"$device.displayName Parsing raw command: \" + description\n    \n    def result = null\n       \n    // zwave.parse: \n    // The second parameter specifies which command version to return for each command type.\n\t// See: https://graph.api.smartthings.com/ide/doc/zwave-utils.html\n    // PAN04 supports:\n    //  COMMAND_CLASS_BASIC [0x20: 1]\n    //  COMMAND_CLASS_SWITCH_BINARY [0x25: 1]\n    //  COMMAND_CLASS_SWITCH_ALL [0x27: 1]\n    //  COMMAND_CLASS_METER_V3 [0x32: 3]\n\t//  COMMAND_CLASS_MULTI_CHANNEL_V3 [0x60: 3]\n    //  COMMAND_CLASS_CONFIGURATION [0x70: 1]\n\t//  COMMAND_CLASS_ALARM [0x71: 1]\n    //  COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 [0x72: 2]\n    //  COMMAND_CLASS_ASSOCIATION_V1 [0x85: 1]\n\t//  COMMAND_CLASS_VERSION [0x86: 1]\n    //  ...\n\tdef cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x27: 1, 0x32: 3, 0x60: 3, 0x70: 1, 0x71: 1, 0x72: 2, 0x85: 1, 0x86: 1])\n\tif (cmd) {\n\t\tif (state.debug) log.debug \"$device.displayName zwave.parse() returned: $cmd\"\n\t\tresult = zwaveEvent(cmd)\n\t\tif (state.debug) log.debug \"$device.displayName zwaveEvent() returned: ${result?.inspect()}\"\t\n\t}\n\treturn result\n}\n\n/**\n *  COMMAND_CLASS_BASIC (0x20)\n *\n *  Short\tvalue\t0xFF for on, 0x00 for off\n *\n *  The PAN04 will report Basic and Binary Switch reports in different ways \n *  depending on the value of Configuration Parameter #3.\n *\n *  Request a meter report for power if switch has changed state.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd)\n{\n\tdef evt = createEvent(name: \"switch\", value: cmd.value ? \"on\" : \"off\", type: \"physical\")\n\tif (evt.isStateChange) {\n\t\t[evt, response([\"delay 1000\", zwave.meterV3.meterGet(scale: 2).format()])]\n\t} else {\n\t\tevt\n\t}\n}\n\n/**\n *  COMMAND_CLASS_SWITCH_BINARY (0x25)\n *\n *  Short\tvalue\t0xFF for on, 0x00 for off\n *\n *  The PAN04 will report Basic and Binary Switch reports in different ways \n *  depending on the value of Configuration Parameter #3.\n *\n *  Request a meter report for power if switch has changed state.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd)\n{\n\tdef evt = createEvent(name: \"switch\", value: cmd.value ? \"on\" : \"off\", type: \"digital\")\n\tif (evt.isStateChange) {\n\t\t[evt, response([\"delay 1000\", zwave.meterV3.meterGet(scale: 2).format()])]\n\t} else {\n\t\tevt\n\t}\n}\n\n/**\n *  COMMAND_CLASS_METER_V3 (0x32)\n * \n *  Process Meter Report. \n *  If an energy report is received, a voltage report is also requested.\n *  If a power report is received, current and powerFactor reports are reqeusted.\n *\n *  Integer\t\t\tdeltaTime\t\t    \t\tTime in seconds since last report\n *  Short\t\t\tmeterType\t\t    \t\tUnknown = 0, Electric = 1, Gas = 2, Water = 3\n *  List<Short>\t\tmeterValue\t\t    \t\tMeter value as an array of bytes\n *  Double\t\t\tscaledMeterValue\t\t\tMeter value as a double\n *  List<Short>\t\tpreviousMeterValue\t\t\tPrevious meter value as an array of bytes\n *  Double\t\t\tscaledPreviousMeterValue    Previous meter value as a double\n *  Short\t\t\tsize\t\t\t\t\t\tThe size of the array for the meterValue and previousMeterValue\n *  Short\t\t\tscale\t\t\t\t\t\tThe scale of the values: \"kWh\"=0, \"kVAh\"=1, \"Watts\"=2, \"pulses\"=3, \"Volts\"=4, \"Amps\"=5, \"Power Factor\"=6, \"Unknown\"=7\n *  Short\t\t\tprecision\t\t\t\t\tThe decimal precision of the values\n *  Short\t\t\trateType\t\t\t\t\t???\n *  Boolean\t\t\tscale2\t\t\t\t\t\t???\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {\n\tif (cmd.scale == 0) {\n    \t// Accumulated Energy (kWh) - Update stats and request voltage.\n    \tstate.energy = cmd.scaledMeterValue\n\t\tupdateStats()\n        sendEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" kWh\", displayed: false)\n\t\tdef event = createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kWh\")\n        def cmds = []\n        cmds << \"delay 1000\"\n    \tcmds << zwave.meterV3.meterGet(scale: 4).format() // Request voltage (Volts).\n        return [event, response(cmds)] // return a list containing the event and the result of response(). \n\t} else if (cmd.scale == 1) {\n    \t// Accumulated Energy (kVAh) - Ignore.\n\t\t//createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kVAh\")\n\t} else if (cmd.scale == 2) {\n    \t// Instantaneous Power (Watts) - Record power, and requst current & powerFactor.\n\t\tsendEvent(name: \"dispPower\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" W\", displayed: false)\n        def event = createEvent(name: \"power\", value: cmd.scaledMeterValue, unit: \"W\")\n        def cmds = []\n        cmds << \"delay 1000\"\n    \tcmds << zwave.meterV3.meterGet(scale: 5).format() // Request current (Amps).\n        cmds << \"delay 1000\"\n    \tcmds << zwave.meterV3.meterGet(scale: 6).format() // Request powerFactor.\n        return [event, response(cmds)] // return a list containing the event and the result of response().\n\t} else if (cmd.scale == 4) {\n    \t// Instantaneous Voltage (Volts)\n\t\tsendEvent(name: \"dispVoltage\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" V\", displayed: false)\n        return createEvent(name: \"voltage\", value: cmd.scaledMeterValue, unit: \"V\")\n\t} else if (cmd.scale == 5) { \n    \t// Instantaneous Current (Amps)\n\t\tsendEvent(name: \"dispCurrent\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" A\", displayed: false)\n        return createEvent(name: \"current\", value: cmd.scaledMeterValue, unit: \"A\")\n\t} else if (cmd.scale == 6) {\n    \t// Instantaneous Power Factor\n\t\tsendEvent(name: \"dispPowerFactor\", value: \"PF: \" + String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal), displayed: false)\n        return createEvent(name: \"powerFactor\", value: cmd.scaledMeterValue, unit: \"PF\")\n\t}\n}\n\n/**\n *  COMMAND_CLASS_CONFIGURATION (0x70)\n *\n *  Log received configuration values.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {\n\n\t// Translate value (byte array) back to scaledConfigurationValue (decimal):\n    // This should be done in zwave.parse() but isn't implemented yet.\n    // See: https://community.smartthings.com/t/zwave-configurationv2-configurationreport-dev-question/9771/6\n    // I can't make this work just yet...\n\t//int value = java.nio.ByteBuffer.wrap(cmd.configurationValue as byte[]).getInt()\n    // Instead, a brute force way\n    def scValue = 0\n    if (cmd.size == 1) { scValue = cmd.configurationValue[0]}\n    else if (cmd.size == 2) {  scValue = cmd.configurationValue[1] + (cmd.configurationValue[0] * 0x100) }\n    else if (cmd.size == 3) {  scValue = cmd.configurationValue[2] + (cmd.configurationValue[1] * 0x100) + (cmd.configurationValue[0] * 0x10000) }\n    else if (cmd.size == 4) {  scValue = cmd.configurationValue[3] + (cmd.configurationValue[2] * 0x100) + (cmd.configurationValue[1] * 0x10000) + (cmd.configurationValue[0] * 0x1000000) }\n\n    // Translate parameterNumber to parameterDescription:\n    def parameterDescription\n    switch (cmd.parameterNumber) {\n        case 1:\n            parameterDescription = \"Power Report Interval (x5sec)\"\n            break\n        case 2:\n            parameterDescription = \"Energy Report Interval (x10min)\"\n            break\n        case 3:\n            parameterDescription = \"Selected End Point For Basic Commands\"\n            break\n        case 4:\n            parameterDescription = \"Manual Switch Mode\"\n            break\n        case 5:\n            parameterDescription = \"Power Threshold for Load Caution (W)\"\n            break\n        case 6:\n            parameterDescription = \"Energy Threshold for Load Caution (kWh)\"\n            break\n        default:\n            parameterDescription = \"Unknown Parameter\"\n\t}\n    \n\t//log.debug \"$device.displayName: Configuration Report: parameterNumber: $cmd.parameterNumber, parameterDescription: $parameterDescription, size: $cmd.size, scaledConfigurationValue: $scValue\"\n\tcreateEvent(descriptionText: \"$device.displayName: Configuration Report: parameterNumber: $cmd.parameterNumber, parameterDescription: $parameterDescription, size: $cmd.size, scaledConfigurationValue: $scValue\", displayed: false)\n}\n\n/**\n *  COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 (0x72)\n *\n *  \n **/\ndef zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {\n\tdef msr = String.format(\"%04X-%04X-%04X\", cmd.manufacturerId, cmd.productTypeId, cmd.productId)\n\tif (state.debug) log.debug \"$device.displayName: MSR: $msr\"\n\tupdateDataValue(\"MSR\", msr)\n\n\t// Apply Manufacturer- or Product-specific configuration here...\n}\n\n/**\n *  Default event handler.\n *\n *  Called for all events that aren't handled above.\n **/\ndef zwaveEvent(physicalgraph.zwave.Command cmd) {\n\tif (state.debug) log.debug \"$device.displayName: Unhandled: $cmd\"\n\t[:]\n}\n\n\n/**********************************************************************\n *  Capability-related Commands:\n **********************************************************************/\n\n/**\n *  on() - Turns the switch on.\n *\n *  Required for the \"Switch\" capability.\n **/\ndef on() {\n\t[\n\t\tzwave.basicV1.basicSet(value: 0xFF).format(),\n\t\tzwave.switchBinaryV1.switchBinaryGet().format(),\n\t\t\"delay 1000\",\n\t\tzwave.meterV3.meterGet(scale: 2).format()\n\t]\n}\n\n\n/**\n *  off() - Turns the switch off.\n *\n *  Required for the \"Switch\" capability.\n **/\ndef off() {\n\t[\n\t\tzwave.basicV1.basicSet(value: 0x00).format(),\n\t\tzwave.switchBinaryV1.switchBinaryGet().format(),\n\t\t\"delay 1000\",\n\t\tzwave.meterV3.meterGet(scale: 2).format()\n\t]\n}\n\n\n/**\n *  refresh() - Refreshes values from the device. Same as poll()?\n *\n *  Required for the \"Refresh\" capability.\n **/\ndef refresh() {\n\tdelayBetween([\n\t\tzwave.switchBinaryV1.switchBinaryGet().format(),\n\t\tzwave.meterV3.meterGet(scale: 0).format(), // Energy\n\t\tzwave.meterV3.meterGet(scale: 2).format() // Power\n\t\t//zwave.meterV3.meterGet(scale: 4).format(), // Volts - Not included, as a request will be triggered when energy report is received.\n\t\t//zwave.meterV3.meterGet(scale: 5).format(), // Current - Not included, as a request will be triggered when power report is received.\n\t\t//zwave.meterV3.meterGet(scale: 6).format() // Power Factor - Not included, as a request will be triggered when power report is received.\n\t])\n}\n\n\n/**\n *  poll() - Polls the device.\n *\n *  Required for the \"Polling\" capability\n **/\ndef poll() {\n\trefresh()\n}\n\n\n/**\n *  reset() - Reset the Accumulated Energy figure held in the device.\n *\n *  Custom energy reporting period stats are preserved.\n **/\ndef reset() {\n\tif (state.debug) log.debug \"Reseting Accumulated Energy\"\n    state.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"lastReset\", value: state.lastReset, unit: \"\")\n\t\n\t// Record energy<Period> in energy<Period>Prev:\n\tstate.energyTodayPrev = state.energyToday\n\tstate.energyTodayStart = 0.00\n\tstate.energyMonthPrev = state.energyMonth\n\tstate.energyMonthStart = 0.00\n\tstate.energyYearPrev = state.energyYear\n\tstate.energyYearStart = 0.00\n    state.energyLifetimePrev = state.energyLifetime\n\tstate.energy = 0.00\n    \n    return [\n\t\tzwave.meterV3.meterReset().format(),\n\t\t\"delay 1000\",\n\t\tzwave.meterV3.meterGet(scale: 0).format()\n\t]\n}\n\n\n/**********************************************************************\n *  Other Commands:\n **********************************************************************/\n\n\n/**\n *  resetAllStats() - Reset all Accumulated Energy statistics (!)\n *\n *  Resets the Accumulated Energy figure held in the device AND resets all custom energy reporting period stats!\n **/\ndef resetAllStats() {\n\tif (state.debug) log.debug \"Reseting All Accumulated Energy Stats!\"\n    state.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"lastReset\", value: state.lastReset, unit: \"\")\n\t\n\t// Reset all energy<Period>Prev/Start values:\n\tstate.energyTodayPrev = 0.00\n\tstate.energyTodayStart = 0.00\n\tstate.energyMonthPrev = 0.00\n\tstate.energyMonthStart = 0.00\n\tstate.energyYearPrev = 0.00\n\tstate.energyYearStart = 0.00\n    state.energyLifetimePrev = 0.00\n\tstate.energy = 0.00\n    \n    return [\n\t\tzwave.meterV3.meterReset().format(),\n\t\t\"delay 1000\",\n\t\tzwave.meterV3.meterGet(scale: 0).format()\n\t]\n}\n\n\n/**\n *  installed() - Runs when the device is first installed.\n **/\ndef installed() {\n\tlog.debug \"${device.displayName}: Installing.\"\n\tstate.installedAt = now()\n\tstate.energy = 0\n\tstate.costPerKWH = 0\n\tstate.costOfEnergy = 0\n\tstate.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    state.statsMode = 'Today'\n\tstate.secondaryInfo = 'Single Mode'\n\tsendEvent(name: \"secondaryInfo\", value: state.secondaryInfo, displayed: false)\n}\n\n\n/**\n *  updated() - Runs when you hit \"Done\" from \"Edit Device\".\n * \n *  Weirdly, it seems to be called twice after hitting \"Done\"!\n * \n *  Note, the updated() method is not a 'command', so it doesn't send commands by default.\n *  To execute commands from updated() you have to specifically return a HubAction object. \n *  The response() helper wraps commands up in a HubAction so they can be sent from parse() or updated().\n *  See: https://community.smartthings.com/t/remotec-z-thermostat-configuration-with-z-wave-commands/31956/12\n **/\ndef updated() {\n\n\tlog.debug \"Updated() called\"\n\t// Update internal state:\n\tstate.debug = (\"true\" == configDebugMode)\n\tstate.costPerKWH = configCostPerKWH as BigDecimal\n\tstate.secondaryInfo = 'Single Mode'\n\tsendEvent(name: \"secondaryInfo\", value: state.secondaryInfo, displayed: false)\n    \n    return response( [configure() , refresh() ])\n}\n\n/**\n *  updateStats() - Recalculates energy and cost for each reporting period.\n *\n *  All costs are calculated at the prevailing rate.\n *\n *   Attributes:\n *    energy                = Energy (kWh) as reported by device (ad hoc period). [Native Energy Meter attribute].\n *    costOfEnergy          = Cost of energy (ad hoc period).\n *    energyToday           = Accumulated energy (today only).\n *    costOfEnergyToday     = Cost of energy (today).\n *    energy24Hours         = Accumulated energy (last 24 hours).\n *    costOfEnergy24Hours   = Cost of energy (last 24 hours).\n *    energy7Days           = Accumulated energy (last 7 days).\n *    costOfEnergy7Days     = Cost of energy (last 7 days).\n *    energyMonth           = Accumulated energy (this month).\n *    costOfEnergyMonth     = Cost of energy (this month).\n *    energyYear            = Accumulated energy (this year).\n *    costOfEnergyYear      = Cost of energy (this year).\n *    energyLifetime        = Accumulated energy (lifetime).\n *    costOfEnergyLifetime  = Cost of energy (lifetime).\n *   \n *   Private State:\n *    costPerKWH            = Unit cost as specified by user in settings.\n *    reportingPeriod       = YYYY/MM/dd of current reporting period.\n *    energyTodayStart      = energy that was reported at the start of today. Will be zero if ad hoc period has been reset today.\n *    energyTodayPrev       = energy that was reported today, prior to lastReset. Will be zero if ad hoc period has not been reset today.\n *    energyMonthStart      = energy that was reported at the start of this month. Will be zero if ad hoc period has been reset this month.\n *    energyMonthPrev       = energy that was reported this month, prior to lastReset. Will be zero if ad hoc period has not been reset this month.\n *    energyYearStart       = energy that was reported at the start of this year. Will be zero if ad hoc period has been reset this year.\n *    energyYearPrev        = energy that was reported this year, prior to lastReset. Will be zero if ad hoc period has not been reset this year.\n *    energyLifetimePrev    = energy that was reported this lifetime, prior to lastReset. Will be zero if ad hoc period has never been reset.\n *   \n **/\nprivate updateStats() {\n\n\tif (state.debug) log.debug \"${device.displayName}: Updating Statistics\"\n\t\n\tif (!state.energy) {state.energy = 0}\n\tif (!state.costPerKWH) {state.costPerKWH = 0}\n\tif (!state.reportingPeriod) {state.reportingPeriod = \"Uninitialised\"}\n\tif (!state.energyTodayStart) {state.energyTodayStart = 0}\n\tif (!state.energyTodayPrev) {state.energyTodayPrev = 0}\n\tif (!state.energyMonthStart) {state.energyMonthStart = 0}\n\tif (!state.energyMonthPrev) {state.energyMonthPrev = 0}\n\tif (!state.energyYearStart) {state.energyYearStart = 0}\n\tif (!state.energyYearPrev) {state.energyYearPrev = 0}\n\tif (!state.energyLifetimePrev) {state.energyLifetimePrev = 0}\n\t\n\t// Check if reportingPeriod has changed (i.e. it's a new day):\n\tdef today = new Date().format(\"YYYY/MM/dd\", location.timeZone)\n\tif ( today != state.reportingPeriod) {\n\t\t// It's a new Reporting Period:\n\t\tlog.info \"${device.displayName}: New Reporting Period: ${today}\"\n        \n        // Check if new year:\n\t\tif ( today.substring(0,4) != state.reportingPeriod.substring(0,4)) {\n        \tstate.energyYearStart = state.energy\n\t\t\tstate.energyYearPrev = 0.00\n        }\n\n        // Check if new month:\n\t\tif ( today.substring(0,7) != state.reportingPeriod.substring(0,7)) {\n        \tstate.energyMonthStart = state.energy\n\t\t\tstate.energyMonthPrev = 0.00\n        }\n\n        // Daily rollover:\n\t\tstate.energyTodayStart = state.energy\n\t\tstate.energyTodayPrev = 0.00\n        \n        // Update reportingPeriod:\n        state.reportingPeriod = today\n\t}\n\t\n    // energy (ad hoc period):\n    // Nothing to caclulate, just need to update dispEnergy:\n    sendEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",state.energy as BigDecimal) + \" kWh\", displayed: false)\n    \n    // costOfEnergy (ad hoc period):\n\ttry {\n\t\tstate.costOfEnergy = state.energy * state.costPerKWH\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy: £${state.costOfEnergy}\"\n\t\tsendEvent(name: \"costOfEnergy\", value: state.costOfEnergy, unit: \"£\")\n        sendEvent(name: \"dispCostOfEnergy\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy as BigDecimal), displayed: false)\n\t} catch (e) { log.debug e }\n\n\t// energyToday:\n\ttry {\n\t\tstate.energyToday = state.energy + state.energyTodayPrev - state.energyTodayStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy Today: ${state.energyToday} kWh\"\n\t\tsendEvent(name: \"energyToday\", value: state.energyToday, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyToday:\n\ttry {\n\t\tstate.costOfEnergyToday = (state.energyToday * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Today: £${state.costOfEnergyToday}\"\n\t\tsendEvent(name: \"costOfEnergyToday\", value: state.costOfEnergyToday, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyMonth:\n\ttry {\n\t\tstate.energyMonth = state.energy + state.energyMonthPrev - state.energyMonthStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Month: ${state.energyMonth} kWh\"\n\t\tsendEvent(name: \"energyMonth\", value: state.energyMonth, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyMonth:\n\ttry {\n\t\tstate.costOfEnergyMonth = (state.energyMonth * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Month: £${state.costOfEnergyMonth}\"\n\t\tsendEvent(name: \"costOfEnergyMonth\", value: state.costOfEnergyMonth, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyYear:\n\ttry {\n\t\tstate.energyYear = state.energy + state.energyYearPrev - state.energyYearStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Year: ${state.energyYear} kWh\"\n\t\tsendEvent(name: \"energyYear\", value: state.energyYear, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyYear:\n\ttry {\n\t\tstate.costOfEnergyYear = (state.energyYear * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Year: £${state.costOfEnergyYear}\"\n\t\tsendEvent(name: \"costOfEnergyYear\", value: state.costOfEnergyYear, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyLifetime:\n\ttry {\n\t\tstate.energyLifetime = state.energy + state.energyLifetimePrev\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Lifetime: ${state.energyLifetime} kWh\"\n\t\tsendEvent(name: \"energyLifetime\", value: state.energyLifetime, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyLifetime:\n\ttry {\n\t\tstate.costOfEnergyLifetime = (state.energyLifetime * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Lifetime: £${state.costOfEnergyLifetime}\"\n\t\tsendEvent(name: \"costOfEnergyLifetime\", value: state.costOfEnergyLifetime, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    // Moving Periods - Calculated by looking up previous values of energyLifetime:\n    \n    // energy24Hours:\n\ttry {\n    \t// We need the last value of energyLifetime that is at least 24 hours old.\n\t\t//  We get previous values of energyLifetime between 1 and 7 days old, in case the device has been off for a while.\n\t\t//  So long as the device reported energy back at least once during this period, we should get a result.\n        //  As results are returned in reverse chronological order, we just need the first 1 record.\n\t\t\n        // Use a calendar object to create offset dates:\n\t\tCalendar cal = new GregorianCalendar()\n\t\tcal.add(Calendar.DATE, -1 )\n\t\tDate end = cal.getTime()\n\t\tcal.add(Calendar.DATE, -6 )\n\t\tDate start = cal.getTime()\n\n\t\tdef previousELStates = device.statesBetween(\"energyLifetime\", start, end,[max: 1])\n\t\tdef previousEL\n    \tif (previousELStates) { \n        \tpreviousEL = previousELStates[previousELStates.size -1].value as BigDecimal \n            if (state.debug) log.debug \"${device.displayName}: energyLifetime 24 Hours Ago was: ${previousEL} kWh\"\n        }\n    \telse { \n        \tpreviousEL = 0.0 \n        \tif (state.debug) log.debug \"${device.displayName}: No value for energyLifetime 24 Hours Ago!\"\n        }\n    \tif (previousEL > state.energyLifetime) { previousEL = 0.0 } // If energyLifetime has been reset, discard previous value.\n        \n    \tstate.energy24Hours = state.energyLifetime - previousEL\n        if (state.debug) log.debug \"${device.displayName}: Energy Last 24 Hours: ${state.energy24Hours} kWh\"\n\t\tsendEvent(name: \"energy24Hours\", value: state.energy24Hours, unit: \"kWh\")\n\t} catch (e) { log.debug e }    \n    \n\t// costOfEnergy24Hours:\n\ttry {\n\t\tstate.costOfEnergy24Hours = (state.energy24Hours * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Last 24 Hours: £${state.costOfEnergy24Hours}\"\n\t\tsendEvent(name: \"costOfEnergy24Hours\", value: state.costOfEnergy24Hours, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    \n    // energy7Days:\n\ttry {\n    \t// We need the last value of energyLifetime, up to 7 old (previous states are only kept for 7 days).\n\t\t//  We get previous values of energyLifetime between 6 and 7 days old, in case the device has been off for a while.\n\t\t//  So long as the device reported energy back at least once during this period, we should get a result.\n        //  As results are returned in reverse chronological order, we need the last record, so we request the max of 1000.\n\t\t//  If there were more than 1000 updates between start and end, we won't get the oldest one,\n        //  however stats should normally only be generated every 10 mins at most.\n\t\t\n    \t// Use a calendar object to create offset dates:\n\t\tCalendar cal = new GregorianCalendar()\n\t\tcal.add(Calendar.DATE, -6 )\n\t\tDate end = cal.getTime()\n\t\tcal.add(Calendar.DATE, -1 )\n\t\tDate start = cal.getTime()\n\n\t\t// Get previous values of energyLifetime between 7 Days and 6 days 23 hours old: \n\t\tdef previousELStates = device.statesBetween(\"energyLifetime\", start, end,[max: 1000])\n\t\tdef previousEL\n    \tif (previousELStates) { \n        \tpreviousEL = previousELStates[previousELStates.size -1].value as BigDecimal \n            if (state.debug) log.debug \"${device.displayName}: energyLifetime 7 Days Ago was: ${previousEL} kWh\"\n        }\n    \telse { \n        \tpreviousEL = 0.0 \n        \tif (state.debug) log.debug \"${device.displayName}: No value for energyLifetime 7 Days Ago!\"\n        }\n    \tif (previousEL > state.energyLifetime) { previousEL = 0.0 } // If energyLifetime has been reset, discard previous value.\n        \n    \tstate.energy7Days = state.energyLifetime - previousEL\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy Last 7 Days: ${state.energy7Days} kWh\"\n\t\tsendEvent(name: \"energy7Days\", value: state.energy7Days, unit: \"kWh\")\n\t} catch (e) { log.debug e }    \n    \n\t// costOfEnergy7Days:\n\ttry {\n\t\tstate.costOfEnergy7Days = (state.energy7Days * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Last 7 Days: £${state.costOfEnergy7Days}\"\n\t\tsendEvent(name: \"costOfEnergy7Days\", value: state.costOfEnergy7Days, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    \n    //disp<>Period:\n    if ('Today' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyToday as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyToday as BigDecimal), displayed: false)\n    }\n    if ('Last 24 Hours' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy24Hours as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy24Hours as BigDecimal), displayed: false)\n    }\n    if ('Last 7 Days' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy7Days as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy7Days as BigDecimal), displayed: false)\n    }\n    if ('This Month' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyMonth as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyMonth as BigDecimal), displayed: false)\n    }\n    if ('This Year' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyYear as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyYear as BigDecimal), displayed: false)\n    }\n    if ('Lifetime' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyLifetime as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyLifetime as BigDecimal), displayed: false)\n    }\n    \n}\n\n/**\n *  cycleStats() - Cycle displayed statistics period.\n **/\ndef cycleStats() {\n\tif (state.debug) log.debug \"$device.displayName: Cycling Stats\"\n\t\n    if ('Today' == state.statsMode) {\n    \tstate.statsMode = 'Last 24 Hours'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy24Hours as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy24Hours as BigDecimal), displayed: false)\n    }\n    else if ('Last 24 Hours' == state.statsMode) {\n    \tstate.statsMode = 'Last 7 Days'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy7Days as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy7Days as BigDecimal), displayed: false)\n    }\n    else if ('Last 7 Days' == state.statsMode) {\n    \tstate.statsMode = 'This Month'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyMonth as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyMonth as BigDecimal), displayed: false)\n    }\n    else if ('This Month' == state.statsMode) {\n    \tstate.statsMode = 'This Year'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyYear as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyYear as BigDecimal), displayed: false)\n    }\n    else if ('This Year' == state.statsMode) {\n    \tstate.statsMode = 'Lifetime'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyLifetime as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyLifetime as BigDecimal), displayed: false)\n    }\n    else  {\n    \tstate.statsMode = 'Today'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyToday as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyToday as BigDecimal), displayed: false)\n    }\n    \n\tsendEvent(name: \"statsMode\", value: state.statsMode, displayed: false)\n\tif (state.debug) log.debug \"$device.displayName: StatsMode changed to: ${state.statsMode}\"\n\t\n}\n\n\n/**\n *  configure() - Configure physical device parameters.\n *\n *  Gets values from the Preferences section.\n **/\t\t\t\ndef configure() {\n    \n    if (state.debug) log.debug \"$device.displayName: Configuring Device\"\n    \n    // Build Commands based on input preferences:\n    // Some basic validation is done, if any values are out of range they're set back to default.\n    //  It doesn't seem possible to read the defaultValue of each input from $settings, so default values are duplicated here.\n    def cmds = []\n    \n\t// Auto-Reporting:\n\tif (\"true\" == configAutoReport) {\n\t\t// Add this hub's ID to Group 1 so that Power and Energy auto reports are sent to the hub:\n\t\tcmds << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId).format()\n\t\tif (state.debug) log.debug \"$device.displayName: Enabling Auto-Reporting\"\n\t}\n\telse {\n\t\t// Remove Hub's ID from Group 1 (auto-reports will not be received by the hub):\n        cmds << zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format()\n\t\tif (state.debug) log.debug \"$device.displayName: Disabling Auto-Reporting\"\n\t}\n    //cmds << zwave.associationV1.associationGet(groupingIdentifier:1).format()\n    \n    // Parameter 1 - Power Report Interval (x5sec):\n\tLong CP1 = configParameter1 as Long  \n    if ((CP1 == null) || (CP1 < 1) || (CP1 > 32767)) { CP1 = 12 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 1, size: 2, scaledConfigurationValue: CP1).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format()\n    \n    // Parameter 2 - Energy Report Interval (x10min):\n    Long CP2 = configParameter2 as Long\n    if ((CP2 == null) || (CP2 < 1) || (CP2 > 32767)) { CP2 = 1 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 2, size: 2, scaledConfigurationValue: CP2).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format()\n    \n    // Parameter 3 - Selected End Point For Basic Commands:\n    Long CP3 \n    if (configParameter3 == \"Relay 1 & 2\") {CP3 = 1}\n\telse if (configParameter3 == \"Relay 1\") {CP3 = 2}\n\telse if (configParameter3 == \"Relay 2\") {CP3 = 3}\n\telse {CP3 = 1}\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: CP3).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format()\n    \n    // Parameter 4 - Manual Switch Mode:\n    Long CP4 \n    if (configParameter4 == \"Edge\") {CP4 = 1}\n\telse if (configParameter4 == \"Pulse\") {CP4 = 2}\n\telse if (configParameter4 == \"Edge-Toggle\") {CP4 = 3}\n\telse {CP4 = 1}\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: CP4).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 4).format()\n    \n    // Parameter 5 - Power Threshold for Load Caution (W):\n    Long CP5 = configParameter5 as Long\n    if ((CP5 == null) || (CP5 < 10) || (CP5 > 1500)) { CP5 = 1500 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 5, size: 2, scaledConfigurationValue: CP5).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 5).format()\n    \n\t// Parameter 6 - Energy Threshold for Load Caution (kWh):\n    Long CP6 = configParameter6 as Long\n    if ((CP6 == null) || (CP6 < 1) || (CP6 > 10000)) { CP6 = 10000 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 2, scaledConfigurationValue: CP6).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 6).format()\n    \n    // Return:\n    if ( cmds != [] && cmds != null ) return delayBetween(cmds, 500) else return\n}\n\n/**\n *  test() - Temp testing method.\n **/\ndef test() {\n\tif (state.debug) log.debug \"$device.displayName: Testing\"\n\n}"
  },
  {
    "path": "devices/tkb-metering-switch/tkb-metering-switch.groovy",
    "content": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: TKB Metering Switch (TZ88E-GEN5)\n *\n *  Author: David Lomas (codersaur)\n *\n *  Date: 2016-10-10\n *\n *  Version: 1.11\n *\n *  Description:\n *   - This device handler is written specifically for the TKB Metering Switch (TZ88E-GEN5).\n *   - Supports live reporting of energy, power, current, voltage, and powerFactor. Press the 'Now' tile to refresh.\n *      (voltage and powerFactor tiles are not shown by default, but you can enable them below).\n *   - Supports reporting of energy usage and cost over an ad hoc period, based on the 'energy' figure reported by \n *     the device. Press the 'Since...' tile to reset.\n *   - Supports additional reporting of energy usage and cost over multiple pre-defined periods:\n *       'Today', 'Last 24 Hours', 'Last 7 Days', 'This Month', 'This Year', and 'Lifetime'\n *     These can be cycled through by pressing the 'statsMode' tile. There's also a tile that will reset all Energy\n *     Stats periods, but it's hidden by default.\n *   - All configurable device parameters can be set from the device settings. Refer to the TZ88E-GEN5 instruction \n *     manual for full details.\n *   - The Multi-tile will indicate if the physical switch is enabled/disabled, or if RF command behaviour is altered.\n *   - If you are re-using this device, please use your own hosting for the icons.\n *\n *  TZ88E-GEN5 device notes:\n *   - Auto-Meter-Reports for power and energy are sent to association group 1. The hub needs to be added to\n *     this group to receive these auto-reports (this is done for you if you enable 'Enable Auto-Reporting' in\n *     the device settings).\n *   - The device cannot be configured to send auto-reports for voltage, current, or powerFactor. \n *     Therefore, meter reports for current and powerFactor are requested whenever a meter report for power is received.\n *     Additionally, a meter report for voltage is reqeusted whenever a meter report for energy is received.\n *\n *  Version History:\n *\n *   2016-10-10: v1.11\n *    - 'Voltage Measurement' capability is now accepted.\n *\n *   2016-03-02: v1.10\n *    - Meter reports for current and powerFactor are requested whenever a meter report for power is received.\n *    - Meter reports for voltage are reqeusted whenever a meter report for energy is received.\n *\n *   2016-03-01: v1.09\n *    - Cleaned up parse() method.\n *\n *   2016-02-28: v1.08\n *    - Fixed required properties on input parameters.\n *\n *   2016-02-14: v1.07\n *    - General tidy up.\n *    - poll() now just calls refresh().\n *    - standardised date format in installed().\n * \n *   2016-02-12: v1.06\n *    - New Icons, hosted on GitHub.\n *    - A meter report for current is now requested whenever a meter report for power is received.\n *    - Fixed execution of commands in configure() when called from updated(), so a 'configure' tile is not needed.\n *    - resetAllStats() method to reset all Accumulated Energy statistics! Corresponding tile is hidden by default. \n *\n *   2016-02-11: v1.05\n *    - Improved calculation of energy24Hours.\n *\n *   2016-02-10: v1.04\n *    - Added energy<> and costOfEnergy<> stats for 'Last 24 Hours' and 'Last 7 Days'.\n *\n *   2016-02-09: v1.03\n *    - Added energy<> and costOfEnergy<> stats for Month/Year/Lifetime.\n *    - statsMode tile now cycles through stats modes.\n *    - Fixed formatting of displayed values by using disp* attributes (yuk).\n *    - Secondary information on Multi-tile indicates if switch is enabled/disabled, or RF command behaviour is altered.\n *\n *   2016-02-08: v1.02\n *    - Added energyToday & costOfEnergyToday stats.\n *    - All stats calculation moved to updateStats().\n *\n *   2016-02-07: v1.01\n *    - Added ConfigurationReport event parser.\n *    - Added configurable settings for all device parameters.\n *    - Added multi-attribute tile.\n *    - Added support for Voltage, Current, and Power Factor.\n *    - Added Total Cost, based on CostPerKWh setting.\n *\n *   2016-02-06: v1.0 - Initial Version for TZ88E-GEN5.\n *    - Added fingerprint for TZ88E-GEN5.\n * \n *  To Do:\n *   - Option to specify a '£/day' fixed charge, which is added to all energy cost calculations.\n *   - Process Alarm reports.\n *   - Add Min/Max/Ave stats (instMode tile to cycle through: Now/Min/Max/Ave).\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n **/\n\nmetadata {\n\tdefinition (name: \"TKB Metering Switch (TZ88E-GEN5)\", namespace: \"codersaur\", author: \"David Lomas\") {\n\t\tcapability \"Actuator\"\n\t\tcapability \"Switch\"\n\t\tcapability \"Power Meter\"\n\t\tcapability \"Energy Meter\"\n\t\tcapability \"Voltage Measurement\"\n\t\tcapability \"Polling\"\n\t\tcapability \"Refresh\"\n\t\tcapability \"Configuration\"\n\t\tcapability \"Sensor\"\n\n\t\tcommand \"reset\"\n        command \"refresh\"\n        command \"configure\"\n        command \"updated\"\n        command \"poll\"\n        command \"cycleStats\"\n\t\tcommand \"resetAllStats\"\n\t\tcommand \"test\"\n        \n\t\t// Standard (Capability) Attributes:\n\t\tattribute \"switch\", \"string\"\n        attribute \"power\", \"number\"\n        attribute \"energy\", \"number\" // Energy (kWh) as reported by device (ad hoc period).\n        \n        // Custom Attributes:\n        attribute \"current\", \"number\"\n        attribute \"voltage\", \"number\"\n        attribute \"powerFactor\", \"number\"\n\t\tattribute \"lastReset\", \"string\" // Time that ad hoc reporting was reset.\n\t\tattribute \"statsMode\", \"string\"\n\t\tattribute \"costOfEnergy\", \"number\" \n\t\tattribute \"energyToday\", \"number\"\n\t\tattribute \"costOfEnergyToday\", \"number\"\n\t\tattribute \"energy24Hours\", \"number\"\n\t\tattribute \"costOfEnergy24Hours\", \"number\"\n\t\tattribute \"energy7Days\", \"number\"\n\t\tattribute \"costOfEnergy7Days\", \"number\"\n\t\tattribute \"energyMonth\", \"number\"\n\t\tattribute \"costOfEnergyMonth\", \"number\"\n\t\tattribute \"energyYear\", \"number\"\n\t\tattribute \"costOfEnergyYear\", \"number\"\n\t\tattribute \"energyLifetime\", \"number\"\n\t\tattribute \"costOfEnergyLifetime\", \"number\"\n        attribute \"secondaryInfo\", \"string\"\n        \n        // Display Attributes:\n        // These are only required because the UI lacks number formatting and strips leading zeros.\n        attribute \"dispPower\", \"string\"\n        attribute \"dispCurrent\", \"string\"\n        attribute \"dispVoltage\", \"string\"\n        attribute \"dispPowerFactor\", \"string\"\n        attribute \"dispEnergy\", \"string\"\n        attribute \"dispCostOfEnergy\", \"string\"\n        attribute \"dispEnergyPeriod\", \"string\"\n        attribute \"dispCostOfEnergyPeriod\", \"string\"\n        \n        // Fingerprints:\n\t\tfingerprint deviceId:\"0x1001\", inClusters:\"0x5E 0x86 0x72 0x98 0x5A 0x85 0x59 0x73 0x25 0x20 0x27 0x32 0x70 0x71 0x75 0x7A\"\n\t}\n\n\t// Tile definitions:\n\ttiles(scale: 2) {\n    \n\t\t// Main Tiles:\n        standardTile(\"switch\", \"device.switch\", width: 2, height: 2, decoration: \"flat\", canChangeIcon: true) {\n\t\t\tstate \"on\", label: '${name}', action: \"switch.off\", icon: \"st.switches.switch.on\", backgroundColor: \"#79b821\"\n\t\t\tstate \"off\", label: '${name}', action: \"switch.on\", icon: \"st.switches.switch.off\", backgroundColor: \"#ffffff\"\n\t\t}\n        \n        // Multi Tile:\n\t\tmultiAttributeTile(name:\"multi1\", type: \"generic\", width: 4, height: 4, canChangeIcon: true) {\n\t\t\ttileAttribute (\"device.switch\", key: \"PRIMARY_CONTROL\") {\n\t\t\t\tattributeState \"on\", label: '${name}', action: \"switch.off\", icon: \"st.switches.switch.on\", backgroundColor: \"#79b821\"\n\t\t\t\tattributeState \"off\", label: '${name}', action: \"switch.on\", icon: \"st.switches.switch.off\", backgroundColor: \"#ffffff\"\n\t\t\t}\n\t\t\ttileAttribute (\"device.secondaryInfo\", key: \"SECONDARY_CONTROL\") {\n\t\t\t\tattributeState \"default\", label:'${currentValue}'\n\t\t\t}\n\t\t}\n\t\t\n\t\t// Instantaneous Values:\n\t\tvalueTile(\"instMode\", \"device.dispPower\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Now:', action:\"refresh.refresh\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_refresh.png\"\n\t\t}\n\t\tvalueTile(\"power\", \"device.dispPower\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"current\", \"device.dispCurrent\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"voltage\", \"device.dispVoltage\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"powerFactor\", \"device.dispPowerFactor\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t\n        // Ad Hoc Energy Stats:\n\t\tvalueTile(\"lastReset\", \"device.lastReset\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Since:  ${currentValue}', action:\"reset\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_stopwatch_reset.png\"\n\t\t}\n\t\tvalueTile(\"energy\", \"device.dispEnergy\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costOfEnergy\", \"device.dispCostOfEnergy\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t\n\t\t// Energy Stats:\n        // Needs to be a standardTile to be able to change icon for each state.\n\t\tvalueTile(\"statsMode\", \"device.statsMode\", decoration: \"flat\", canChangeIcon: true, canChangeBackground: true, width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"Today\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"Last 24 Hours\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"Last 7 Days\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n            state \"This Month\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"This Year\", label:\"${currentValue}:\", action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t\tstate \"Lifetime\", label:'${currentValue}:', action: \"cycleStats\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png\"\n\t\t}\n\t\tvalueTile(\"energyPeriod\", \"device.dispEnergyPeriod\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costOfEnergyPeriod\", \"device.dispCostOfEnergyPeriod\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\tvalueTile(\"costPerKWH\", \"device.costPerKWH\", decoration: \"flat\", width: 2, height: 1) {\n\t\t\tstate \"default\", label:'Unit Cost: ${currentValue}', icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png\"\n\t\t}\n\t\t\n\t\t// Action Buttons:\n\t\tstandardTile(\"refresh\", \"device.power\", inactiveLabel: false, decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'', action:\"refresh.refresh\", icon:\"st.secondary.refresh\"\n\t\t}\n\t\tstandardTile(\"resetAllStats\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'RESET ALL STATS!', action:\"resetAllStats\"\n\t\t}\n\t\tstandardTile(\"configure\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'', action:\"configuration.configure\", icon:\"st.secondary.configure\"\n\t\t}\n\t\tstandardTile(\"test\", \"device.power\", decoration: \"flat\", width: 2, height: 2) {\n\t\t\tstate \"default\", label:'Test', action:\"test\"\n\t\t}\n\t\t\n\t\t// Tile layouts:\n\t\tmain([\"switch\",\"power\",\"energy\"])\n\t\tdetails([\n\t\t\t// Multi Tile:\n\t\t\t\"multi1\"\n\t\t\t// Instantaneous Values:\n\t\t\t,\"instMode\",\"power\", \"current\" //,\"voltage\", \"powerFactor\"\n\t\t\t// Ad Hoc Stats:\n\t\t\t,\"lastReset\", \"energy\", \"costOfEnergy\"\t\n\t\t\t// Energy Stats:\n\t\t\t,\"statsMode\", \"energyPeriod\", \"costOfEnergyPeriod\" //,\"costPerKWH\"\n\t\t\t// Action Buttons:\n\t\t\t//, \"refresh\",\"resetAllStats\",\"configure\",\"test\"\n\t\t])\n\t}\n    \n    preferences {\n    \t\n        input \"configCostPerKWH\", \"string\", title: \"Energy Cost (£/kWh)\", defaultValue: \"0.1253\", required: true, displayDuringSetup: true\n    \tinput \"configAutoReport\", \"boolean\", title: \"Enable Auto-Reporting?\", defaultValue: true, required: false, displayDuringSetup: true\n\n\t\t// Device Configuration Parameters:\n    \tinput \"configParameter1\", \"number\", title: \"Power Report Interval (x5sec):\", defaultValue: 12, required: false, displayDuringSetup: true // 1 min.\n    \tinput \"configParameter2\", \"number\", title: \"Energy Report Interval (x10min):\", defaultValue: 1, required: false, displayDuringSetup: true // 10 min.\n        input \"configParameter3\", \"number\", title: \"Current Threshold for Load Caution (x0.01A):\", defaultValue: 1300, required: false, displayDuringSetup: true\n        input \"configParameter4\", \"number\", title: \"Energy Threshold for Load Caution (kWh):\", defaultValue: 10000, required: false, displayDuringSetup: true\n        input \"configParameter5\", \"enum\", title: \"Restore Switch State Mode:\", \n\t\t\toptions:[\"Last State\", \"Off\", \"On\"], defaultValue: \"Last State\", required: false, displayDuringSetup: true\n        input \"configParameter6\", \"boolean\", title: \"Enable Switch?\", defaultValue: true, required: false, displayDuringSetup: true\n    \tinput \"configParameter7\", \"enum\", title: \"LED Indication Mode:\", \n\t\t\toptions:[\"Show Switch State\", \"Night Mode\"], defaultValue: \"Show Switch State\", required: false, displayDuringSetup: true\n        input \"configParameter8\", \"number\", title: \"Auto-Off Timer (s):\", defaultValue: 0, required: false, displayDuringSetup: true\n        input \"configParameter9\", \"enum\", title: \"RF Off Command Mode:\", \n\t\t\toptions:[\"Switch Off\", \"Ignore\", \"Toggle State\", \"Switch On\"], defaultValue: \"Switch Off\", required: false, displayDuringSetup: true\t\t\n        \n\t\t// Debug Mode:\n\t\tinput \"configDebugMode\", \"boolean\", title: \"Enable debug logging?\", defaultValue: true, required: false, displayDuringSetup: true\n    }\n}\n\n\n/**********************************************************************\n *  Z-wave Event Handlers.\n **********************************************************************/\n\n/**\n *  parse() - Called when messages from a device are received by the hub.\n *\n *  The parse method is responsible for interpreting those messages and returning Event definitions.\n *\n *  String \t\tdescription \t\t- The message from the device.\n **/\ndef parse(String description) {\n\tif (state.debug) log.debug \"$device.displayName Parsing raw command: \" + description\n    \n    def result = null\n    \n\t// zwave.parse: \n    // The second parameter specifies which command version to return for each command type:\n    // TZ88E-GEN5 supports:\n    //  COMMAND_CLASS_BASIC [0x20: 1]\n    //  COMMAND_CLASS_SWITCH_BINARY [0x25: 1]\n    //  COMMAND_CLASS_METER_V3 [0x32: 3]\n    //  COMMAND_CLASS_CONFIGURATION [0x70: 1]\n    //  COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 [0x72: 2]\n    //  ...\n\tdef cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x32: 3, 0x70: 1, 0x72: 2])\n\tif (cmd) {\n\t\tif (state.debug) log.debug \"$device.displayName zwave.parse() returned: $cmd\"\n\t\tresult = zwaveEvent(cmd)\n\t\tif (state.debug) log.debug \"$device.displayName zwaveEvent() returned: ${result?.inspect()}\"\t\n\t}\n\treturn result\n}\n\n/**\n *  COMMAND_CLASS_BASIC (0x20)\n *\n *  Short\tvalue\t0xFF for on, 0x00 for off\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd)\n{\n\tdef evt = createEvent(name: \"switch\", value: cmd.value ? \"on\" : \"off\", type: \"physical\")\n\tif (evt.isStateChange) {\n\t\t[evt, response([\"delay 1000\", zwave.meterV2.meterGet(scale: 2).format()])]\n\t} else {\n\t\tevt\n\t}\n}\n\n/**\n *  COMMAND_CLASS_SWITCH_BINARY (0x25)\n *\n *  Short\tvalue\t0xFF for on, 0x00 for off\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd)\n{\n\tdef evt = createEvent(name: \"switch\", value: cmd.value ? \"on\" : \"off\", type: \"digital\")\n\tif (evt.isStateChange) {\n\t\t[evt, response([\"delay 1000\", zwave.meterV3.meterGet(scale: 2).format()])]\n\t} else {\n\t\tevt\n\t}\n}\n\n/**\n *  COMMAND_CLASS_METER_V3 (0x32)\n * \n *  Process Meter Report. \n *  If an energy report is received, a voltage report is also requested.\n *  If a power report is received, current and powerFactor reports are reqeusted.\n *\n *  Integer\t\t\tdeltaTime\t\t    \t\tTime in seconds since last report\n *  Short\t\t\tmeterType\t\t    \t\tUnknown = 0, Electric = 1, Gas = 2, Water = 3\n *  List<Short>\t\tmeterValue\t\t    \t\tMeter value as an array of bytes\n *  Double\t\t\tscaledMeterValue\t\t\tMeter value as a double\n *  List<Short>\t\tpreviousMeterValue\t\t\tPrevious meter value as an array of bytes\n *  Double\t\t\tscaledPreviousMeterValue    Previous meter value as a double\n *  Short\t\t\tsize\t\t\t\t\t\tThe size of the array for the meterValue and previousMeterValue\n *  Short\t\t\tscale\t\t\t\t\t\tThe scale of the values: \"kWh\"=0, \"kVAh\"=1, \"Watts\"=2, \"pulses\"=3, \"Volts\"=4, \"Amps\"=5, \"Power Factor\"=6, \"Unknown\"=7\n *  Short\t\t\tprecision\t\t\t\t\tThe decimal precision of the values\n *  Short\t\t\trateType\t\t\t\t\t???\n *  Boolean\t\t\tscale2\t\t\t\t\t\t???\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {\n\tif (cmd.scale == 0) {\n    \t// Accumulated Energy (kWh) - Update stats and request voltage.\n    \tstate.energy = cmd.scaledMeterValue\n\t\tupdateStats()\n        sendEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal) + \" kWh\", displayed: false)\n\t\tdef event = createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kWh\")\n        def cmds = []\n        cmds << \"delay 1000\"\n    \tcmds << zwave.meterV3.meterGet(scale: 4).format() // Request voltage (Volts).\n        return [event, response(cmds)] // return a list containing the event and the result of response(). \n\t} else if (cmd.scale == 1) {\n    \t// Accumulated Energy (kVAh) - Ignore.\n\t\t//createEvent(name: \"energy\", value: cmd.scaledMeterValue, unit: \"kVAh\")\n\t} else if (cmd.scale == 2) {\n    \t// Instantaneous Power (Watts) - Record power, and requst current & powerFactor.\n\t\tsendEvent(name: \"dispPower\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" W\", displayed: false)\n        def event = createEvent(name: \"power\", value: cmd.scaledMeterValue, unit: \"W\")\n        def cmds = []\n        cmds << \"delay 1000\"\n    \tcmds << zwave.meterV3.meterGet(scale: 5).format() // Request current (Amps).\n        cmds << \"delay 1000\"\n    \tcmds << zwave.meterV3.meterGet(scale: 6).format() // Request powerFactor.\n        return [event, response(cmds)] // return a list containing the event and the result of response().\n\t} else if (cmd.scale == 4) {\n    \t// Instantaneous Voltage (Volts)\n\t\tsendEvent(name: \"dispVoltage\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" V\", displayed: false)\n        return createEvent(name: \"voltage\", value: cmd.scaledMeterValue, unit: \"V\")\n\t} else if (cmd.scale == 5) { \n    \t// Instantaneous Current (Amps)\n\t\tsendEvent(name: \"dispCurrent\", value: String.format(\"%.1f\",cmd.scaledMeterValue as BigDecimal) + \" A\", displayed: false)\n        return createEvent(name: \"current\", value: cmd.scaledMeterValue, unit: \"A\")\n\t} else if (cmd.scale == 6) {\n    \t// Instantaneous Power Factor\n\t\tsendEvent(name: \"dispPowerFactor\", value: \"PF: \" + String.format(\"%.2f\",cmd.scaledMeterValue as BigDecimal), displayed: false)\n        return createEvent(name: \"powerFactor\", value: cmd.scaledMeterValue, unit: \"PF\")\n\t}\n}\n\n/**\n *  COMMAND_CLASS_CONFIGURATION (0x70)\n *\n *  Log received configuration values.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {\n\n\t// Translate value (byte array) back to scaledConfigurationValue (decimal):\n    // This should be done in zwave.parse() but isn't implemented yet.\n    // See: https://community.smartthings.com/t/zwave-configurationv2-configurationreport-dev-question/9771/6\n    // I can't make this work just yet...\n\t//  int value = java.nio.ByteBuffer.wrap(cmd.configurationValue as byte[]).getInt()\n    // Instead, a brute force way\n    def scValue = 0\n    if (cmd.size == 1) { scValue = cmd.configurationValue[0]}\n    else if (cmd.size == 2) {  scValue = cmd.configurationValue[1] + (cmd.configurationValue[0] * 0x100) }\n    else if (cmd.size == 3) {  scValue = cmd.configurationValue[2] + (cmd.configurationValue[1] * 0x100) + (cmd.configurationValue[0] * 0x10000) }\n    else if (cmd.size == 4) {  scValue = cmd.configurationValue[3] + (cmd.configurationValue[2] * 0x100) + (cmd.configurationValue[1] * 0x10000) + (cmd.configurationValue[0] * 0x1000000) }\n\n    // Translate parameterNumber to parameterDescription:\n    def parameterDescription\n    switch (cmd.parameterNumber) {\n        case 1:\n            parameterDescription = \"Power Report Interval (x5sec)\"\n            break\n        case 2:\n            parameterDescription = \"Energy Report Interval (x10min)\"\n            break\n        case 3:\n            parameterDescription = \"Current Threshold for Load Caution (x0.01A)\"\n            break\n        case 4:\n            parameterDescription = \"Energy Threshold for Load Caution (kWh)\"\n            break\n        case 5:\n            parameterDescription = \"Restore Switch State Mode\"\n            break\n        case 6:\n            parameterDescription = \"Enable Switch\"\n            break\n        case 7:\n            parameterDescription = \"LED Indication Mode\"\n            break\n        case 8:\n            parameterDescription = \"Auto-Off Timer (s)\"\n            break\n        case 9:\n            parameterDescription = \"RF Off Command Mode\"\n            break\n        default:\n            parameterDescription = \"Unknown Parameter\"\n\t}\n    \n\t//log.debug \"$device.displayName: Configuration Report: parameterNumber: $cmd.parameterNumber, parameterDescription: $parameterDescription, size: $cmd.size, scaledConfigurationValue: $scValue\"\n\tcreateEvent(descriptionText: \"$device.displayName: Configuration Report: parameterNumber: $cmd.parameterNumber, parameterDescription: $parameterDescription, size: $cmd.size, scaledConfigurationValue: $scValue\", displayed: false)\n}\n\n/**\n *  COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 (0x72)\n *\n *  \n **/\ndef zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {\n\tdef msr = String.format(\"%04X-%04X-%04X\", cmd.manufacturerId, cmd.productTypeId, cmd.productId)\n\tif (state.debug) log.debug \"$device.displayName: MSR: $msr\"\n\tupdateDataValue(\"MSR\", msr)\n\n\t// Apply Manufacturer- or Product-specific configuration here...\n}\n\n/**\n *  Default event handler.\n *\n *  Called for all events that aren't handled above.\n **/\ndef zwaveEvent(physicalgraph.zwave.Command cmd) {\n\tif (state.debug) log.debug \"$device.displayName: Unhandled: $cmd\"\n\t[:]\n}\n\n\n/**********************************************************************\n *  Capability-related Commands:\n **********************************************************************/\n\n/**\n *  on() - Turns the switch on.\n *\n *  Required for the \"Switch\" capability.\n **/\ndef on() {\n\t[\n\t\tzwave.basicV1.basicSet(value: 0xFF).format(),\n\t\tzwave.switchBinaryV1.switchBinaryGet().format(),\n\t\t\"delay 1000\",\n\t\tzwave.meterV3.meterGet(scale: 2).format()\n\t]\n}\n\n\n/**\n *  off() - Turns the switch off.\n *\n *  Required for the \"Switch\" capability.\n **/\ndef off() {\n\t[\n\t\tzwave.basicV1.basicSet(value: 0x00).format(),\n\t\tzwave.switchBinaryV1.switchBinaryGet().format(),\n\t\t\"delay 1000\",\n\t\tzwave.meterV3.meterGet(scale: 2).format()\n\t]\n}\n\n\n/**\n *  refresh() - Refreshes values from the device. Same as poll()?\n *\n *  Required for the \"Refresh\" capability.\n **/\ndef refresh() {\n\tdelayBetween([\n\t\tzwave.switchBinaryV1.switchBinaryGet().format(),\n\t\tzwave.meterV3.meterGet(scale: 0).format(), // Energy\n\t\tzwave.meterV3.meterGet(scale: 2).format() // Power\n\t\t//zwave.meterV3.meterGet(scale: 4).format(), // Volts - Not included, as a request will be triggered when energy report is received.\n\t\t//zwave.meterV3.meterGet(scale: 5).format(), // Current - Not included, as a request will be triggered when power report is received.\n\t\t//zwave.meterV3.meterGet(scale: 6).format() // Power Factor - Not included, as a request will be triggered when power report is received.\n\t])\n}\n\n\n/**\n *  poll() - Polls the device.\n *\n *  Required for the \"Polling\" capability\n **/\ndef poll() {\n\trefresh()\n}\n\n\n/**\n *  reset() - Reset the Accumulated Energy figure held in the device.\n *\n *  Custom energy reporting period stats are preserved.\n **/\ndef reset() {\n\tif (state.debug) log.debug \"Reseting Accumulated Energy\"\n    state.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"lastReset\", value: state.lastReset, unit: \"\")\n\t\n\t// Record energy<Period> in energy<Period>Prev:\n\tstate.energyTodayPrev = state.energyToday\n\tstate.energyTodayStart = 0.00\n\tstate.energyMonthPrev = state.energyMonth\n\tstate.energyMonthStart = 0.00\n\tstate.energyYearPrev = state.energyYear\n\tstate.energyYearStart = 0.00\n    state.energyLifetimePrev = state.energyLifetime\n\tstate.energy = 0.00\n    \n    return [\n\t\tzwave.meterV3.meterReset().format(),\n\t\t\"delay 1000\",\n\t\tzwave.meterV3.meterGet(scale: 0).format()\n\t]\n}\n\n\n/**********************************************************************\n *  Other Commands:\n **********************************************************************/\n\n\n/**\n *  resetAllStats() - Reset all Accumulated Energy statistics (!)\n *\n *  Resets the Accumulated Energy figure held in the device AND resets all custom energy reporting period stats!\n **/\ndef resetAllStats() {\n\tif (state.debug) log.debug \"Reseting All Accumulated Energy Stats!\"\n    state.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    sendEvent(name: \"lastReset\", value: state.lastReset, unit: \"\")\n\t\n\t// Reset all energy<Period>Prev/Start values:\n\tstate.energyTodayPrev = 0.00\n\tstate.energyTodayStart = 0.00\n\tstate.energyMonthPrev = 0.00\n\tstate.energyMonthStart = 0.00\n\tstate.energyYearPrev = 0.00\n\tstate.energyYearStart = 0.00\n    state.energyLifetimePrev = 0.00\n\tstate.energy = 0.00\n    \n    return [\n\t\tzwave.meterV3.meterReset().format(),\n\t\t\"delay 1000\",\n\t\tzwave.meterV3.meterGet(scale: 0).format()\n\t]\n}\n\n\n/**\n *  installed() - Runs when the device is first installed.\n **/\ndef installed() {\n\tlog.debug \"${device.displayName}: Installing.\"\n\tstate.installedAt = now()\n\tstate.energy = 0\n\tstate.costPerKWH = 0\n\tstate.costOfEnergy = 0\n\tstate.lastReset = new Date().format(\"YYYY/MM/dd \\n HH:mm:ss\", location.timeZone)\n    state.statsMode = 'Today'\n}\n\n\n/**\n *  updated() - Runs when you hit \"Done\" from \"Edit Device\".\n * \n *  Weirdly, it seems to be called twice after hitting \"Done\"!\n * \n *  Note, the updated() method is not a 'command', so it doesn't send commands by default.\n *  To execute commands from updated() you have to specifically return a HubAction object. \n *  The response() helper wraps commands up in a HubAction so they can be sent from parse() or updated().\n *  See: https://community.smartthings.com/t/remotec-z-thermostat-configuration-with-z-wave-commands/31956/12\n **/\ndef updated() {\n\n\tlog.debug \"${device.displayName}: Updated()\"\n\t// Update internal state:\n\tstate.debug = (\"true\" == configDebugMode)\n\tstate.costPerKWH = configCostPerKWH as BigDecimal\n    \n    // Update secondaryInfo:\n    if (configParameter6 == \"false\") { state.secondaryInfo = \"Switch is Disabled (Meter Only)\" }\n\telse if (configParameter9 == \"Ignore\") { state.secondaryInfo = \"RF Commands Disabled!\" }\n\telse if (configParameter9 == \"Toggle State\") { state.secondaryInfo = \"RF Commands Toggle Switch!\" }\n\telse if (configParameter9 == \"Switch On\") { state.secondaryInfo = \"RF Commands Reversed!\" }\n    else { state.secondaryInfo = \"\\n\" }\n\tsendEvent(name: \"secondaryInfo\", value: state.secondaryInfo, displayed: false)\n    \n \treturn response( [configure() , refresh() ])\n}\n\n/**\n *  updateStats() - Recalculates energy and cost for each reporting period.\n *\n *  All costs are calculated at the prevailing rate.\n *\n *   Attributes:\n *    energy                = Energy (kWh) as reported by device (ad hoc period). [Native Energy Meter attribute].\n *    costOfEnergy          = Cost of energy (ad hoc period).\n *    energyToday           = Accumulated energy (today only).\n *    costOfEnergyToday     = Cost of energy (today).\n *    energy24Hours         = Accumulated energy (last 24 hours).\n *    costOfEnergy24Hours   = Cost of energy (last 24 hours).\n *    energy7Days           = Accumulated energy (last 7 days).\n *    costOfEnergy7Days     = Cost of energy (last 7 days).\n *    energyMonth           = Accumulated energy (this month).\n *    costOfEnergyMonth     = Cost of energy (this month).\n *    energyYear            = Accumulated energy (this year).\n *    costOfEnergyYear      = Cost of energy (this year).\n *    energyLifetime        = Accumulated energy (lifetime).\n *    costOfEnergyLifetime  = Cost of energy (lifetime).\n *   \n *   Private State:\n *    costPerKWH            = Unit cost as specified by user in settings.\n *    reportingPeriod       = YYYY/MM/dd of current reporting period.\n *    energyTodayStart      = energy that was reported at the start of today. Will be zero if ad hoc period has been reset today.\n *    energyTodayPrev       = energy that was reported today, prior to lastReset. Will be zero if ad hoc period has not been reset today.\n *    energyMonthStart      = energy that was reported at the start of this month. Will be zero if ad hoc period has been reset this month.\n *    energyMonthPrev       = energy that was reported this month, prior to lastReset. Will be zero if ad hoc period has not been reset this month.\n *    energyYearStart       = energy that was reported at the start of this year. Will be zero if ad hoc period has been reset this year.\n *    energyYearPrev        = energy that was reported this year, prior to lastReset. Will be zero if ad hoc period has not been reset this year.\n *    energyLifetimePrev    = energy that was reported this lifetime, prior to lastReset. Will be zero if ad hoc period has never been reset.\n *   \n **/\nprivate updateStats() {\n\n\tif (state.debug) log.debug \"${device.displayName}: Updating Statistics\"\n\t\n\tif (!state.energy) {state.energy = 0}\n\tif (!state.costPerKWH) {state.costPerKWH = 0}\n\tif (!state.reportingPeriod) {state.reportingPeriod = \"Uninitialised\"}\n\tif (!state.energyTodayStart) {state.energyTodayStart = 0}\n\tif (!state.energyTodayPrev) {state.energyTodayPrev = 0}\n\tif (!state.energyMonthStart) {state.energyMonthStart = 0}\n\tif (!state.energyMonthPrev) {state.energyMonthPrev = 0}\n\tif (!state.energyYearStart) {state.energyYearStart = 0}\n\tif (!state.energyYearPrev) {state.energyYearPrev = 0}\n\tif (!state.energyLifetimePrev) {state.energyLifetimePrev = 0}\n\t\n\t// Check if reportingPeriod has changed (i.e. it's a new day):\n\tdef today = new Date().format(\"YYYY/MM/dd\", location.timeZone)\n\tif ( today != state.reportingPeriod) {\n\t\t// It's a new Reporting Period:\n\t\tlog.info \"${device.displayName}: New Reporting Period: ${today}\"\n        \n        // Check if new year:\n\t\tif ( today.substring(0,4) != state.reportingPeriod.substring(0,4)) {\n        \tstate.energyYearStart = state.energy\n\t\t\tstate.energyYearPrev = 0.00\n        }\n\n        // Check if new month:\n\t\tif ( today.substring(0,7) != state.reportingPeriod.substring(0,7)) {\n        \tstate.energyMonthStart = state.energy\n\t\t\tstate.energyMonthPrev = 0.00\n        }\n\n        // Daily rollover:\n\t\tstate.energyTodayStart = state.energy\n\t\tstate.energyTodayPrev = 0.00\n        \n        // Update reportingPeriod:\n        state.reportingPeriod = today\n\t}\n\t\n    // energy (ad hoc period):\n    // Nothing to caclulate, just need to update dispEnergy:\n    sendEvent(name: \"dispEnergy\", value: String.format(\"%.2f\",state.energy as BigDecimal) + \" kWh\", displayed: false)\n    \n    // costOfEnergy (ad hoc period):\n\ttry {\n\t\tstate.costOfEnergy = state.energy * state.costPerKWH\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy: £${state.costOfEnergy}\"\n\t\tsendEvent(name: \"costOfEnergy\", value: state.costOfEnergy, unit: \"£\")\n        sendEvent(name: \"dispCostOfEnergy\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy as BigDecimal), displayed: false)\n\t} catch (e) { log.debug e }\n\n\t// energyToday:\n\ttry {\n\t\tstate.energyToday = state.energy + state.energyTodayPrev - state.energyTodayStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy Today: ${state.energyToday} kWh\"\n\t\tsendEvent(name: \"energyToday\", value: state.energyToday, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyToday:\n\ttry {\n\t\tstate.costOfEnergyToday = (state.energyToday * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Today: £${state.costOfEnergyToday}\"\n\t\tsendEvent(name: \"costOfEnergyToday\", value: state.costOfEnergyToday, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyMonth:\n\ttry {\n\t\tstate.energyMonth = state.energy + state.energyMonthPrev - state.energyMonthStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Month: ${state.energyMonth} kWh\"\n\t\tsendEvent(name: \"energyMonth\", value: state.energyMonth, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyMonth:\n\ttry {\n\t\tstate.costOfEnergyMonth = (state.energyMonth * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Month: £${state.costOfEnergyMonth}\"\n\t\tsendEvent(name: \"costOfEnergyMonth\", value: state.costOfEnergyMonth, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyYear:\n\ttry {\n\t\tstate.energyYear = state.energy + state.energyYearPrev - state.energyYearStart\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Year: ${state.energyYear} kWh\"\n\t\tsendEvent(name: \"energyYear\", value: state.energyYear, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyYear:\n\ttry {\n\t\tstate.costOfEnergyYear = (state.energyYear * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Year: £${state.costOfEnergyYear}\"\n\t\tsendEvent(name: \"costOfEnergyYear\", value: state.costOfEnergyYear, unit: \"£\")\n\t} catch (e) { log.debug e }\n\n\t// energyLifetime:\n\ttry {\n\t\tstate.energyLifetime = state.energy + state.energyLifetimePrev\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy This Lifetime: ${state.energyLifetime} kWh\"\n\t\tsendEvent(name: \"energyLifetime\", value: state.energyLifetime, unit: \"kWh\")\n\t} catch (e) { log.debug e }\n\n\t// costOfEnergyLifetime:\n\ttry {\n\t\tstate.costOfEnergyLifetime = (state.energyLifetime * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy This Lifetime: £${state.costOfEnergyLifetime}\"\n\t\tsendEvent(name: \"costOfEnergyLifetime\", value: state.costOfEnergyLifetime, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    // Moving Periods - Calculated by looking up previous values of energyLifetime:\n    \n    // energy24Hours:\n\ttry {\n    \t// We need the last value of energyLifetime that is at least 24 hours old.\n\t\t//  We get previous values of energyLifetime between 1 and 7 days old, in case the device has been off for a while.\n\t\t//  So long as the device reported energy back at least once during this period, we should get a result.\n        //  As results are returned in reverse chronological order, we just need the first 1 record.\n\t\t\n        // Use a calendar object to create offset dates:\n\t\tCalendar cal = new GregorianCalendar()\n\t\tcal.add(Calendar.DATE, -1 )\n\t\tDate end = cal.getTime()\n\t\tcal.add(Calendar.DATE, -6 )\n\t\tDate start = cal.getTime()\n\n\t\tdef previousELStates = device.statesBetween(\"energyLifetime\", start, end,[max: 1])\n\t\tdef previousEL\n    \tif (previousELStates) { \n        \tpreviousEL = previousELStates[previousELStates.size -1].value as BigDecimal \n            if (state.debug) log.debug \"${device.displayName}: energyLifetime 24 Hours Ago was: ${previousEL} kWh\"\n        }\n    \telse { \n        \tpreviousEL = 0.0 \n        \tif (state.debug) log.debug \"${device.displayName}: No value for energyLifetime 24 Hours Ago!\"\n        }\n    \tif (previousEL > state.energyLifetime) { previousEL = 0.0 } // If energyLifetime has been reset, discard previous value.\n        \n    \tstate.energy24Hours = state.energyLifetime - previousEL\n        if (state.debug) log.debug \"${device.displayName}: Energy Last 24 Hours: ${state.energy24Hours} kWh\"\n\t\tsendEvent(name: \"energy24Hours\", value: state.energy24Hours, unit: \"kWh\")\n\t} catch (e) { log.debug e }    \n    \n\t// costOfEnergy24Hours:\n\ttry {\n\t\tstate.costOfEnergy24Hours = (state.energy24Hours * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Last 24 Hours: £${state.costOfEnergy24Hours}\"\n\t\tsendEvent(name: \"costOfEnergy24Hours\", value: state.costOfEnergy24Hours, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    \n    // energy7Days:\n\ttry {\n    \t// We need the last value of energyLifetime, up to 7 old (previous states are only kept for 7 days).\n\t\t//  We get previous values of energyLifetime between 6 and 7 days old, in case the device has been off for a while.\n\t\t//  So long as the device reported energy back at least once during this period, we should get a result.\n        //  As results are returned in reverse chronological order, we need the last record, so we request the max of 1000.\n\t\t//  If there were more than 1000 updates between start and end, we won't get the oldest one,\n        //  however stats should normally only be generated every 10 mins at most.\n\t\t\n    \t// Use a calendar object to create offset dates:\n\t\tCalendar cal = new GregorianCalendar()\n\t\tcal.add(Calendar.DATE, -6 )\n\t\tDate end = cal.getTime()\n\t\tcal.add(Calendar.DATE, -1 )\n\t\tDate start = cal.getTime()\n\n\t\t// Get previous values of energyLifetime between 7 Days and 6 days 23 hours old: \n\t\tdef previousELStates = device.statesBetween(\"energyLifetime\", start, end,[max: 1000])\n\t\tdef previousEL\n    \tif (previousELStates) { \n        \tpreviousEL = previousELStates[previousELStates.size -1].value as BigDecimal \n            if (state.debug) log.debug \"${device.displayName}: energyLifetime 7 Days Ago was: ${previousEL} kWh\"\n        }\n    \telse { \n        \tpreviousEL = 0.0 \n        \tif (state.debug) log.debug \"${device.displayName}: No value for energyLifetime 7 Days Ago!\"\n        }\n    \tif (previousEL > state.energyLifetime) { previousEL = 0.0 } // If energyLifetime has been reset, discard previous value.\n        \n    \tstate.energy7Days = state.energyLifetime - previousEL\n\t\tif (state.debug) log.debug \"${device.displayName}: Energy Last 7 Days: ${state.energy7Days} kWh\"\n\t\tsendEvent(name: \"energy7Days\", value: state.energy7Days, unit: \"kWh\")\n\t} catch (e) { log.debug e }    \n    \n\t// costOfEnergy7Days:\n\ttry {\n\t\tstate.costOfEnergy7Days = (state.energy7Days * state.costPerKWH) as BigDecimal\n\t\tif (state.debug) log.debug \"${device.displayName}: Cost of Energy Last 7 Days: £${state.costOfEnergy7Days}\"\n\t\tsendEvent(name: \"costOfEnergy7Days\", value: state.costOfEnergy7Days, unit: \"£\")\n\t} catch (e) { log.debug e }\n    \n    \n    //disp<>Period:\n    if ('Today' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyToday as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyToday as BigDecimal), displayed: false)\n    }\n    if ('Last 24 Hours' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy24Hours as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy24Hours as BigDecimal), displayed: false)\n    }\n    if ('Last 7 Days' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy7Days as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy7Days as BigDecimal), displayed: false)\n    }\n    if ('This Month' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyMonth as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyMonth as BigDecimal), displayed: false)\n    }\n    if ('This Year' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyYear as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyYear as BigDecimal), displayed: false)\n    }\n    if ('Lifetime' == state.statsMode) {\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyLifetime as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyLifetime as BigDecimal), displayed: false)\n    }\n    \n}\n\n/**\n *  cycleStats() - Cycle displayed statistics period.\n **/\ndef cycleStats() {\n\tif (state.debug) log.debug \"$device.displayName: Cycling Stats\"\n\t\n    if ('Today' == state.statsMode) {\n    \tstate.statsMode = 'Last 24 Hours'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy24Hours as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy24Hours as BigDecimal), displayed: false)\n    }\n    else if ('Last 24 Hours' == state.statsMode) {\n    \tstate.statsMode = 'Last 7 Days'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energy7Days as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergy7Days as BigDecimal), displayed: false)\n    }\n    else if ('Last 7 Days' == state.statsMode) {\n    \tstate.statsMode = 'This Month'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyMonth as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyMonth as BigDecimal), displayed: false)\n    }\n    else if ('This Month' == state.statsMode) {\n    \tstate.statsMode = 'This Year'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyYear as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyYear as BigDecimal), displayed: false)\n    }\n    else if ('This Year' == state.statsMode) {\n    \tstate.statsMode = 'Lifetime'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyLifetime as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyLifetime as BigDecimal), displayed: false)\n    }\n    else  {\n    \tstate.statsMode = 'Today'\n    \tsendEvent(name: \"dispEnergyPeriod\", value: String.format(\"%.2f\",state.energyToday as BigDecimal) + \" kWh\", displayed: false)\n\t\tsendEvent(name: \"dispCostOfEnergyPeriod\", value: \"£\" + String.format(\"%.2f\",state.costOfEnergyToday as BigDecimal), displayed: false)\n    }\n    \n\tsendEvent(name: \"statsMode\", value: state.statsMode, displayed: false)\n\tif (state.debug) log.debug \"$device.displayName: StatsMode changed to: ${state.statsMode}\"\n\t\n}\n\n\n/**\n *  configure() - Configure physical device parameters.\n *\n *  Gets values from the Preferences section.\n **/\ndef configure() {\n    \n    if (state.debug) log.debug \"$device.displayName: Configuring Device\"\n    \n    // Build Commands based on input preferences:\n    // Some basic validation is done, if any values are out of range they're set back to default.\n    //  It doesn't seem possible to read the defaultValue of each input from $settings, so default values are duplicated here.\n    def cmds = []\n    \n\t// Auto-Reporting:\n\tif (\"true\" == configAutoReport) {\n\t\t// Add this hub's ID to Group 1 so that Power and Energy auto reports are sent to the hub:\n\t\tcmds << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId).format()\n\t\tif (state.debug) log.debug \"$device.displayName: Enabling Auto-Reporting\"\n\t}\n\telse {\n\t\t// Remove Hub's ID from Group 1 (auto-reports will not be received by the hub):\n        cmds << zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format()\n\t\tif (state.debug) log.debug \"$device.displayName: Disabling Auto-Reporting\"\n\t}\n    //cmds << zwave.associationV1.associationGet(groupingIdentifier:1).format()\n    \n    // Parameter 1 - Power Report Interval (x5sec):\n\tLong CP1 = configParameter1 as Long  \n    if ((CP1 == null) || (CP1 < 1) || (CP1 > 32767)) { CP1 = 12 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 1, size: 2, scaledConfigurationValue: CP1).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format()\n    \n    // Parameter 2 - Energy Report Interval (x10min):\n    Long CP2 = configParameter2 as Long\n    if ((CP2 == null) || (CP2 < 1) || (CP2 > 32767)) { CP2 = 1 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 2, size: 2, scaledConfigurationValue: CP2).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format()\n    \n    // Parameter 3 - Current Threshold for Load Caution (x0.01A):\n    Long CP3 = configParameter3 as Long\n    if ((CP3 == null) || (CP3 < 10) || (CP3 > 1300)) { CP3 = 1300 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: CP3).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format()\n    \n    // Parameter 4 - Energy Threshold for Load Caution (kWh):\n    Long CP4 = configParameter4 as Long\n    if ((CP4 == null) || (CP4 < 1) || (CP4 > 10000)) { CP4 = 10000 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 2, scaledConfigurationValue: CP4).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 4).format()\n    \n    // Parameter 5 - Restore Switch State Mode:\n\t//  What state will the switch be set to when power is restored?\n    Long CP5 = 1                                  // Last State (Default)\n    if (configParameter5 == \"Off\") {CP5 = 0}      // On\n\telse if (configParameter5 == \"On\") {CP5 = 2}  // Off\n\t\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: CP5).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 5).format()\n    \n    // Set Parameter 6 - Enable Switch?:\n\t// When the switch is disabled, the physical button will not work and z-wave switch on/off commands are also ignored.\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1, scaledConfigurationValue: (\"true\" == configParameter6) ? 1 : 0).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 6).format()\n\n\t// Set Parameter 7 - LED Indication Mode:\n\tLong CP7\n    if (configParameter7 == \"Night Mode\") {CP7 = 2} else {CP7 = 1}\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 7, size: 1, scaledConfigurationValue: CP7).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format()\n\n\t// Parameter 8 - Auto-Off Timer (s):\n\tLong CP8 = configParameter8 as Long  \n    if ((CP8 == null) || (CP8 < 0) || (CP8 > 32767)) { CP8 = 0 }\n    cmds << zwave.configurationV1.configurationSet(parameterNumber: 8, size: 2, scaledConfigurationValue: CP8).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 8).format()\n    \n    // Parameter 9 - RF Off Command Mode:\n    Long CP9 \n    if (configParameter9 == \"Switch Off\") {CP9 = 0}\n\telse if (configParameter9 == \"Ignore\") {CP9 = 1}\n\telse if (configParameter9 == \"Toggle State\") {CP9 = 2}\n\telse if (configParameter9 == \"Switch On\") {CP9 = 3}\n\telse {CP9 = 0}\n\tcmds << zwave.configurationV1.configurationSet(parameterNumber: 9, size: 1, scaledConfigurationValue: CP9).format()\n    cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format()\n    \n    // Return:\n    if ( cmds != [] && cmds != null ) return delayBetween(cmds, 500) else return\n}\n\n/**\n *  test() - Temp testing method.\n **/\ndef test() {\n\tif (state.debug) log.debug \"$device.displayName: Testing\"\n\n}\n"
  },
  {
    "path": "devices/zwave-tweaker/README.md",
    "content": "# Z-Wave Tweaker\nhttps://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker\n\nCopyright (c) [David Lomas](https://github.com/codersaur)\n\n## Overview\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-tiles-main.png\" width=\"200\" align=\"right\">\n\nA SmartThings device handler to assist with interrogating and tweaking Z-Wave devices.\n\n### Key features:\n* Discover association groups, multi-channel endpoints, and configuration parameters.\n* Configure association group members from the SmartThings GUI.\n* Configure parameter values from the SmartThings GUI.\n* Configure Protection and Switch_All modes from the SmartThings GUI.\n* Discover supported meter/alarm/notification/sensor report types.\n* Automatically build a complete list of the Z-Wave commands sent by a device.\n* Support for Z-Wave and Z-Wave Plus devices.\n* Extensive inline code comments to support community development.\n\n## Installation\nThe Z-Wave Tweaker is designed to temporarily replace the normal device handler for a device. Follow [these instructions](https://github.com/codersaur/SmartThings#device-handler-installation-procedure) to install the device handler using the SmartThings IDE.\n\n## GUI\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-tiles-main.png\" width=\"200\" align=\"right\">\n\nThe Z-Wave Tweaker has two main types of tile: _Scan_ tiles and _Print_ tiles.\n\n#### Scan Tiles:\nEach _Scan_ tile triggers interrogation of a certain aspect of the device:\n\n* **Scan General**: Obtains basic properties common to most devices, such as product ID, firmware version, and supported commands.\n* **Scan Association Groups**: Collects information about association groups and their members.\n* **Scan Endpoints**: Scans _endpoints_ advertised by _multi-channel_ devices and discovers their capabilities.\n* **Scan Parameters**: Discovers available configuration parameters, which can be used to customise the device.\n* **Scan Actuator**: Discovers common actuator attributes, such as _basic_, _switch_, and _switchMultiLevel_.\n* **Scan Sensor**: Discovers common sensor capabilities, such as _sensorBinary_, _sensorMultilevel_, _meter_, and _notification_.\n\n#### Print Tiles:\nEach _Print_ tile can be used to output the information collected by the corresponding _Scan_ tile. The output can be viewed using the _Live Logging_ tab within the SmartThings IDE.\n\n#### Sync Tile:\nThis tile indicates when all configuration changes have been successfully synchronised with the physical device.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-tiles-synced.png\" width=\"100\"> <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-tiles-sync-pending.png\" width=\"100\">\n\n#### Cleanup Tile:\nTap this tile when you have finished using the Z-Wave Tweaker. It will remove all collected data in preparation for reinstating the original device handler.\n\n## Usage\nThe Z-Wave Tweaker is designed to be operated from the SmartThings smartphone app, however all of the information collected will be output to the _Live Logging_ tab in the SmartThings IDE.\n\n* Begin by opening the _Live Logging_ tab in the SmartThings IDE. It is recommended to filter the IDE Log so that it shows only the events from the specific device in use. \n* Next, navigate to the device in the SmartThings app on your smartphone.\n\n#### Discovery of Device Properties:\nThe Z-Wave Tweaker can scan a device to discover basic properties, including any supported association groups, multi-channel endpoints, and configuration parameters.\n\n* Tap the _Scan General_ tile to begin collecting basic information. After a few seconds, you should see some responses from the device in the IDE, such as _Version_ and _Protection_ reports.\n* After the responses stop, tap on of the other _Scan_ tiles to begin collecting more-specific information.  \n   Be sure to set appropriate [scan ranges](https://github.com/codersaur/SmartThings/blob/master/devices/zwave-tweaker/README.md#scan-ranges) in the Tweaker's settings, and allow time for each scan to complete.  \n   **Do not run multiple scans at the same time as this will cause network congestion and some responses from the device may be lost**.\n* To view the data that has been collected, tap on the corresponding _Print_ tile. This should output information to the _Live Logging_ tab in the IDE.\n\n   _Print General:_\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-print-general.png\">\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-print-general-stats.png\">\n\n   _Print Association Groups:_\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-print-assocgroups.png\">\n\n   _Print Endpoints:_\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-print-endpoints.png\">\n\n   _Print Parameters:_\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-print-params.png\">\n\n   _Print Commands:_\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-print-commands.png\">\n\n* If the information appears incomplete, try tapping the relevant _Print_ tile again as the IDE sometimes fails to show all messages.\n* If some expected association groups, endpoints, or parameters are not shown, try re-scanning (it's unlikely that all information will be collected successfully on a first scan). Also, check the _settings_ to confirm that appropriate scan ranges are configured.\n\n#### Creating Device Associations:\n_Device Associations_ enable a Z-Wave device to send commands directly to other devices without the commands having to be relayed by the SmartThings hub. For example, a Z-Wave motion sensor may be configured to send a _Basic (ON)_ command to a nearby dimmer device when motion is detected. This direct communication typically gives faster response times compared to triggering rules in a SmartApp.\n\nFor a device to be able to send commands it must support either the ASSOCIATION or MULTI-CHANNEL_ASSOCIATION command classes.  If so, it will have one or more _Association Groups_ that will send certain types of commands on certain conditions. **The operation of these groups will be specific to the device and should be documented in the manufacturer's product manual.**\n\nUsing the Z-Wave Tweaker's settings it is possible to configure one association group at a time:\n\n1. From the SmartThings smartphone app, click on the gear icon to open the device settings.\n2. In the _CONFIGURE ASSOCIATION GROUP_ section, input the ID of the target group, and [fill in the members](https://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker#configure-association-group).   \n   If you want to remove all members from the association group, leave the members blank.  \n   Note, the _Device Network IDs_ for all Z-Wave devices in your SmartThings network are displayed on the _My Devices_ tab in the SmartThings IDE. Consult the relevant manufacturer's manual for information about the endpoints supported by a particular target device.\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-settings-assocgroup.png\" width=\"200\">\n3. If needed, specify the command class to be used (this is not normally required as the Z-Wave Tweaker will automatically select the appropriate command class).\n4. Tap _Done_. The change will now be synced with the device, when complete, the _Sync_ tile should turn green.  \n   In the IDE, you should see the old members and the new members displayed:\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-assocgroupsync.png\">\n5. If a change to an association group will not sync, check the following:\n   * The device supports ASSOCIATION, and if you are setting endpoint destinations MULTI-CHANNEL ASSOCIATION command classes.\n   * The association group ID exists.\n   * The association groups supports the required number of members (it is common for an association groups to support a maximum of 5-8 destinations).\n6. Repeat steps 1-5 for each association group that you wish to change.\n7. Finally, tap the _Print Association Groups_ tile to verify the configuration of all groups.\n\n#### Changing a Device Parameter:\nZ-Wave device parameters can be used to alter the behaviour of a device, for example, a reporting interval, or a sensor threshold. **All parameters are device-specific so it is essential to consult the manufacturer's product manual for a full description of each parameter.**\n\nUsing the Z-Wave Tweaker's settings it is possible to configure one parameter at a time:\n\n1. From the SmartThings smartphone app, click on the gear icon to open the device settings.\n2. In the _CONFIGURE A PARAMETER_ section, input the parameter ID and desired parameter value.\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-settings-param.png\" width=\"200\">\n3. Tap _Done_. The change will now be synced with the device, when complete, the _Sync_ tile should turn green.  \n   In the IDE, you should see the old value and the new value displayed:\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-paramsync.png\">\n4. If a change to a parameter value fails to sync, check the following:\n   * The device supports the CONFIGURATION command class.\n   * The parameter ID is correct.\n   * The parameter is not a read-only parameter.\n   * The value is in the allowed range.\n5. Repeat steps 1-4 for each parameter value that you wish to change.\n6. Finally, tap the _Print Parameters_ tile to verify the configuration of all parameters.\n\n#### Configuring _Protection_ Mode:\nDevices that support the Z-Wave PROTECTION Command Class can be configured to prevent unintentional control (e.g. by a child) by disabling the physical switches and/or RF control.\n\nUsing the Z-Wave Tweaker's settings it is possible to configure both the _Local_ and _RF_ protection mode:\n\n1. From the SmartThings smartphone app, click on the gear icon to open the device settings.\n2. In the _CONFIGURE OTHER SETTINGS_ section, select the desired mode for _Local_ and _RF_ protection.\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-settings-protection.png\" width=\"200\">\n3. Tap _Done_. The change will now be synced with the device, when complete, the _Sync_ tile should turn green.\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-protection-sync.png\">\n4. If a change to the protection mode fails to sync, check the following:\n   * The device supports the PROTECTION command class.\n   * The device supports the specific mode selected (e.g. _Sequence_ is likely to be supported by keypads, but not by simple toggle switches).\n\n#### Configuring Switch-All Mode:\nDevices that support the Z-Wave SWITCH_ALL Command Class can be configured to respond or ignore certain SWITCH_ALL_SET broadcast commands.\n\nUsing the Z-Wave Tweaker's settings it is possible to configure a device's response to SWITCH_ALL commands:\n\n1. From the SmartThings smartphone app, click on the gear icon to open the device settings.\n2. In the _CONFIGURE OTHER SETTINGS_ section, select the desired mode for _ALL ON / ALL OFF_ function.\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-settings-switch-all.png\" width=\"200\">\n3. Tap _Done_. The change will now be synced with the device, when complete, the _Sync_ tile should turn green.\n\n   <img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-log-switch-all-sync.png\">\n4. If a change to the SWITCH_ALL mode fails to sync, check the following:\n   * The device supports the SWITCH_ALL command class.\n   * The device supports the specific mode selected.\n\n\n## Settings\n\n#### General Settings:\n\n* **IDE Live Logging Level**: Set the level of log messages shown in the SmartThings IDE _Live Logging_ tab. For normal operation _Info_ is recommended, if troubleshooting use _Debug_ or _Trace_.\n\n#### Scan Ranges:\nConfigure the scan range for association groups, endpoints, and configuration parameters. If not configured, the default scan ranges are:\n* Association Groups: 0 to 10.\n* Endpoints: 0 to 10.\n* Parameters: 0 to 20.\n\n#### Configure Association Group:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-settings-assocgroup.png\" width=\"200\" align=\"right\">\n\nUse the settings in this section to configure an association group.\n\n* **Association Group ID**: The ID of the group that will be configured. If this input is left blank, no association groups will by modified.\n\n* **Association Group Members**: Members must be defined as a comma-delimited list of targets. Each target device can be specified in one of two ways:\n   * _Node_: A single hexadecimal number (e.g. \"0C\") representing the target _Device Network ID_.\n   * _Endpoint_: A pair of hexadecimal numbers separated by a colon (e.g. \"10:1\") that represent the target _Device Network ID_ and _Endpoint ID_ respectively. For devices that support multiple endpoints (e.g. a dual relay), this allows a specific endpoint to be targeted by the association group.  \n   \n   Note, the Device Network IDs for all Z-Wave devices in your SmartThings network are displayed on the My Devices tab in the SmartThings IDE. Consult the relevant manufacturer's manual for information about the endpoints supported by a particular target device.\n\n* **Command Class**: The Z-Wave Tweaker will automatically detect whether to use _Association_ or _Multi-channel Association_ commands, however you can force it to use a specific command class using this setting.\n\n#### Configure A Parameter:\n\nUse the settings in this section to configure a configuration parameter. Parameters are device-specific so it is recommended to consult the manufacturer's product manual for a full description of each parameter.\n\n* **Parameter ID**: The ID of the parameter that will be configured. If this input is left blank, no parameter values will by modified.\n\n* **Parameter Value**: Enter the desired value for the parameter.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-settings-param.png\" width=\"200\">\n\n#### Configure Other Settings:\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-settings-protection.png\" width=\"200\" align=\"right\">\n\n* **Local Protection**: Prevent unintentional control (e.g. by a child), by disabling any physical switches on the device. The device must support the PROTECTION command class.\n\n* **RF Protection**: Enabling _RF Protection_ means the device will not respond to wireless commands from other Z-Wave devices, including on/off commands issued via the SmartThings app.The device must support the PROTECTION command class.\n\n* **ALL ON/ALL OFF Function**: Control the device's response to SWITCH_ALL_SET commands.\n\n#### Original Settings:\n\nDo not delete any setting values below this line! They belong to the original device handler and will be reinstated when the original device handler is restored.\n\n<img src=\"https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-settings-original.png\" width=\"200\">\n\n## Current Limitations\n* The Z-Wave Tweaker will not work with sleepy (e.g. battery-powered) devices.\n* It is not possible to collect parameter meta-data such as names, descriptions, and value ranges, as SmartThings does not yet support the CONFIGURATION V3 command class.\n\n## Version History\n\n#### 2017-03-16: v0.08\n* initialise(): Removes any null ccIds parsed from the rawDescription.\n\n#### 2017-03-15: v0.07\n* cleanUp(): Uses state.remove() and device.updateSetting()\n\n#### 2017-03-15: v0.06\n* Beta release.\n\n## References\n Some useful links relevant to the development of this device handler:\n* [SmartThings Device Type Developers Guide]( http://docs.smartthings.com/en/latest/device-type-developers-guide/index.html)\n* [Z-Wave Public Specification Files](http://z-wave.sigmadesigns.com/design-z-wave/z-wave-public-specification/)\n\n## License\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n"
  },
  {
    "path": "devices/zwave-tweaker/zwave-tweaker.groovy",
    "content": "/*****************************************************************************************************************\n *  Copyright: David Lomas (codersaur)\n *\n *  Name: Z-Wave Tweaker\n *\n *  Author: David Lomas (codersaur)\n *\n *  Date: 2017-03-16\n *\n *  Version: 0.08\n *\n *  Source: https://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker\n *\n *  Author: David Lomas (codersaur)\n *\n *  Description: A SmartThings device handler to assist with tweaking Z-Wave devices.\n *\n *  For full information, including installation instructions, examples, and version history, see:\n *   https://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n *****************************************************************************************************************/\nmetadata {\n    definition (name: \"Z-Wave Tweaker\", namespace: \"codersaur\", author: \"David Lomas\") {\n        capability \"Actuator\"\n        capability \"Sensor\"\n\n        // Custom Attributes:\n        attribute \"syncPending\", \"number\" // Number of config items that need to be synced with the physical device.\n\n        // Custom Commands:\n        command \"scanGeneral\"\n        command \"scanAssocGroups\"\n        command \"scanEndpoints\"\n        command \"scanParams\"\n        command \"scanActuator\"\n        command \"scanSensor\"\n        command \"printGeneral\"\n        command \"printAssocGroups\"\n        command \"printCommands\"\n        command \"printEndpoints\"\n        command \"printParams\"\n        command \"printActuator\"\n        command \"printSensor\"\n        command \"sync\"\n        command \"cleanUp\"\n\n    }\n\n    tiles(scale: 2) {\n\n        standardTile(\"main\", \"main\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Tweaker', action:\"\", backgroundColor:\"#e86d13\", icon:\"st.secondary.tools\"\n        }\n        standardTile(\"print\", \"print\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'print', action:\"print\"\n        }\n        standardTile(\"scanGeneral\", \"scanGeneral\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Scan General', action:\"scanGeneral\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_wifi.png\"\n        }\n        standardTile(\"scanAssocGroups\", \"scanAssocGroups\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Scan Assoc Grps', action:\"scanAssocGroups\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_wifi.png\"\n        }\n        standardTile(\"scanEndpoints\", \"scanEndpoints\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Scan Endpoints', action:\"scanEndpoints\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_wifi.png\"\n        }\n        standardTile(\"scanParams\", \"scanParams\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Scan Params', action:\"scanParams\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_wifi.png\"\n        }\n        standardTile(\"scanActuator\", \"scanActuator\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Scan Actuator', action:\"scanActuator\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_wifi.png\"\n        }\n        standardTile(\"scanSensor\", \"scanSensor\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Scan Sensor', action:\"scanSensor\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_wifi.png\"\n        }\n        standardTile(\"printGeneral\", \"printGeneral\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Print General', action:\"printGeneral\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_document.png\"\n        }\n        standardTile(\"printAssocGroups\", \"printAssocGroups\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Print Assoc Grps', action:\"printAssocGroups\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_document.png\"\n        }\n        standardTile(\"printEndpoints\", \"printEndpoints\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Print Endpoints', action:\"printEndpoints\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_document.png\"\n        }\n        standardTile(\"printParams\", \"printParams\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Print Params', action:\"printParams\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_document.png\"\n        }\n        standardTile(\"printActuator\", \"printActuator\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Print Actuator', action:\"printActuator\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_document.png\"\n        }\n        standardTile(\"printSensor\", \"printSensor\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Print Sensor', action:\"printSensor\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_document.png\"\n        }\n        standardTile(\"printCommands\", \"printCommands\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Print Commands', action:\"printCommands\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_document.png\"\n        }\n        standardTile(\"syncPending\", \"syncPending\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Sync Pending', backgroundColor:\"#FF6600\", action:\"sync\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_cycle.png\"\n            state \"0\", label:'Synced', backgroundColor:\"#79b821\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_tick.png\"\n        }\n        standardTile(\"cleanUp\", \"cleanUp\", decoration: \"flat\", width: 2, height: 2) {\n            state \"default\", label:'Clean Up', action:\"cleanUp\", icon: \"https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x2_cycle.png\"\n        }\n\n        main([\"main\"])\n        details([\n            \"scanGeneral\",    \"scanAssocGroups\",   \"scanEndpoints\",\n            \"scanParams\",     \"scanActuator\",      \"scanSensor\",\n            \"printGeneral\",   \"printAssocGroups\",  \"printEndpoints\",\n            \"printParams\",    \"printActuator\",     \"printSensor\",\n            \"printCommands\",  \"syncPending\",       \"cleanUp\"\n        ])\n    }\n\n    preferences {\n\n        section { // GENERAL:\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"GENERAL:\",\n                description: \"General device handler settings.\"\n            )\n\n            input (\n                name: \"zwtLoggingLevelIDE\",\n                title: \"IDE Live Logging Level:\\nMessages with this level and higher will be logged to the IDE.\",\n                type: \"enum\",\n                options: [\n                    \"3\" : \"Info\",\n                    \"4\" : \"Debug\",\n                    \"5\" : \"Trace\"\n                ],\n                defaultValue: \"3\",\n                required: false\n            )\n\n        }\n\n        section { // SCAN RANGES:\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"SCAN RANGES:\",\n                description: \"Configure the scanning range for association groups, endpoints, and parameters.\"\n            )\n\n            input (\n                name: \"zwtAssocGroupsScanStart\",\n                title: \"Association Groups Scan Range (Start):\",\n                type: \"number\",\n                range: \"0..255\",\n                required: false\n            )\n\n            input (\n                name: \"zwtAssocGroupsScanStop\",\n                title: \"Association Groups Scan Range (Stop):\",\n                type: \"number\",\n                range: \"0..255\",\n                required: false\n            )\n\n            input (\n                name: \"zwtEndpointsScanStart\",\n                title: \"Endpoints Scan Range (Start):\",\n                type: \"number\",\n                range: \"0..127\",\n                required: false\n            )\n\n            input (\n                name: \"zwtEndpointsScanStop\",\n                title: \"Endpoints Scan Range (Stop):\",\n                type: \"number\",\n                range: \"0..127\",\n                required: false\n            )\n\n            input (\n                name: \"zwtParamsScanStart\",\n                title: \"Parameters Scan Range (Start):\",\n                type: \"number\",\n                range: \"0..65535\",\n                required: false\n            )\n\n            input (\n                name: \"zwtParamsScanStop\",\n                title: \"Parameters Scan Range (Stop):\",\n                type: \"number\",\n                range: \"0..65535\",\n                required: false\n            )\n        }\n\n        section { // CONFIGURE AN ASSOCIATION GROUP:\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"CONFIGURE ASSOCIATION GROUP:\",\n                description: \"Use these settings to configure the members of an association group.\"\n            )\n\n            input (\n                name: \"zwtAssocGroupId\",\n                title: \"Association Group ID:\",\n                type: \"number\",\n                range: \"0..255\",\n                required: false\n            )\n\n            input (\n                name: \"zwtAssocGroupMembers\",\n                title: \"Association Group Members:\\n\" +\n                       \"Enter a comma-delimited list of destinations (node IDs and/or endpoint IDs). \" +\n                       \"All IDs must be in hexadecimal format. E.g.:\\n\" +\n                       \"Node destinations: '11, 0F'\\n\" +\n                       \"Endpoint destinations: '1C:1, 1C:2'\",\n                type: \"text\",\n                required: false\n            )\n            \n            input (\n                name: \"zwtAssocGroupCc\",\n                title: \"Command Class:\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"Auto-detect\",\n                    \"1\" : \"(Single-channel) Association (0x85)\",\n                    \"2\" : \"Multi-Channel Association (0x8E)\"\n                ],\n                required: false\n            )\n\n        }\n\n        section { // CONFIGURE A PARAMETER:\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"CONFIGURE A PARAMETER:\",\n                description: \"Use these settings to configure the value of a device parameter.\"\n            )\n\n            input (\n                name: \"zwtParamId\",\n                title: \"Parameter ID:\",\n                type: \"number\",\n                range: \"0..65536\",\n                required: false\n            )\n\n            input (\n                name: \"zwtParamValue\",\n                title: \"Parameter Value:\",\n                type: \"number\",\n                range: \"-2147483648..2147483647\",\n                required: false\n            )\n\n        }\n\n        section { // OTHER:\n            input type: \"paragraph\",\n                element: \"paragraph\",\n                title: \" CONFIGURE OTHER SETTINGS:\",\n                description: \"Other miscellaneous settings.\"\n\n            input (\n                name: \"zwtProtectLocal\",\n                title: \"Local Protection: Prevent unintentional control (e.g. by a child) by disabling the physical switches:\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"Unprotected\",\n                    \"1\" : \"Protection by sequence\",\n                    \"2\" : \"No operation possible\"\n                ],\n                required: false\n            )\n\n            input (\n                name: \"zwtProtectRF\",\n                title: \"RF Protection: Applies to Z-Wave commands sent from hub or other devices:\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"Unprotected\",\n                    \"1\" : \"No RF control\",\n                    \"2\" : \"No RF response\"\n                ],\n                required: false\n            )\n\n            input (\n                name: \"zwtSwitchAllMode\",\n                title: \"ALL ON/ALL OFF Function:\\nResponse to SWITCH_ALL_SET commands.\",\n                type: \"enum\",\n                options: [\n                    \"0\" : \"0: All ON not active, All OFF not active\",\n                    \"1\" : \"1: All ON not active, All OFF active\",\n                    \"2\" : \"2: All ON active, All OFF not active\",\n                    \"255\" : \"255: All ON active, All OFF active\"],\n                required: false\n            )\n        }\n\n        section {\n\n            input (\n                type: \"paragraph\",\n                element: \"paragraph\",\n                title: \"ORIGINAL SETTINGS:\",\n                description: \"Do not delete any setting values below this line! They belong to the original device \" +\n                \"handler and will be reinstated when the original device handler is restored.\"\n            )\n\n\n\n\n        }\n\n    }\n\n}\n\n/*****************************************************************************************************************\n *  SmartThings System Commands:\n *****************************************************************************************************************/\n\n/**\n *  updated()\n *\n *  Runs when the user hits \"Done\" from Settings page.\n *\n *  Action: Trigger sync of selected parameter and/or association group.\n *\n *  Note: Weirdly, update() seems to be called twice. So execution is aborted if there was a previous execution\n *  within two seconds. See: https://community.smartthings.com/t/updated-being-called-twice/62912\n **/\ndef updated() {\n    logger(\"updated()\",\"trace\")\n\n    def cmds = []\n\n    if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {\n        state.updatedLastRanAt = now()\n\n        // Logging Level:\n        state.loggingLevelIDE = (settings.zwtLoggingLevelIDE) ? settings.zwtLoggingLevelIDE.toInteger() : 3\n\n        // Run initialisation checks\n        if (!state.zwtInitialised) { initialise() }\n\n        // Check if an association group needs to be synced:\n        if (settings.zwtAssocGroupId != null) {\n            // Get MaxSupportedNodes if already in metadata store:\n            def assocGroupMd = state.zwtAssocGroupsMd.find( { it.id == settings.zwtAssocGroupId.toInteger() } )\n            def maxNodes = (assocGroupMd?.maxNodesSupported) ? assocGroupMd?.maxNodesSupported : 256\n            state.zwtAssocGroupTarget = [\n                id: settings.zwtAssocGroupId.toInteger(),\n                nodes: parseAssocGroupInput(settings.zwtAssocGroupMembers,maxNodes),\n                commandClass: (settings.zwtAssocGroupCc) ? settings.zwtAssocGroupCc.toInteger() : 0\n            ]\n        }\n        else {\n            state.zwtAssocGroupTarget = null\n        }\n\n        // Check if a parameter needs to be synced:\n        if ((settings.zwtParamId != null) & (settings.zwtParamValue != null)) {\n            state.zwtParamTarget = [\n                id: settings.zwtParamId.toInteger(),\n                scaledConfigurationValue: settings.zwtParamValue.toInteger()\n            ]\n        }\n        else {\n            state.zwtParamTarget = null\n        }\n\n        sync()\n\n        // Other commands...?\n\n        return sendCommands(cmds)\n    }\n    else {\n        logger(\"updated(): Ran within last 2 seconds so aborting.\",\"debug\")\n    }\n}\n\n/**\n *  parse()\n *\n *  Called when messages from the device are received by the hub. The parse method is responsible for interpreting\n *  those messages and returning event definitions (and command responses).\n *\n *  As this is a Z-wave device, zwave.parse() is used to convert the message into a command. The command is then\n *  passed to zwaveEvent(), which is overloaded for each type of command below.\n *\n *  Note: There is no longer any need to check if description == \"updated\".\n *\n *  Parameters:\n *   String      description        The raw message from the device.\n **/\ndef parse(description) {\n    logger(\"parse(): Parsing raw message: ${description}\",\"trace\")\n\n    def result = []\n    if (!state.zwtCommandsMd) state.zwtCommandsMd = []\n\n    def cmd = zwave.parse(description, getCommandClassVersions())\n    if (cmd) {\n        result += zwaveEvent(cmd)\n    }\n    else {\n        logger(\"parse(): Could not parse raw message: ${description}\",\"error\")\n\n        // Extract details from raw description to add to command meta-data cache:\n        if (description.contains(\"command: \")) {\n            def index = description.indexOf(\"command: \") + 9\n            cmd = [\n                commandClassId: Integer.parseInt(description.substring(index, index +2),16), // Parse as hex.\n                commandId: Integer.parseInt(description.substring(index +2, index +4),16) // Parse as hex.\n            ]\n        }\n    }\n\n    // Update commands meta-data cache:\n    cacheCommandMd(cmd, description)\n\n    return result\n}\n\n/*****************************************************************************************************************\n *  Z-wave Event Handlers.\n *****************************************************************************************************************/\n\n/**\n *  zwaveEvent( COMMAND_CLASS_BASIC (0x20) : BASIC_REPORT (0x03) )\n *\n *  The Basic Report command is used to advertise the status of the primary functionality of the device.\n *\n *  Action: Log info message.\n *\n *  cmd attributes:\n *    Short    value\n *      0x00       = Off\n *      0x01..0x63 = 0..100%\n *      0xFE       = Unknown\n *      0xFF       = On\n *\n *  Example:\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {\n    logger(\"zwaveEvent(): Basic Report received: ${cmd}\",\"info\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_BINARY (0x25) : SWITCH_BINARY_REPORT (0x03) )\n *\n *  The Binary Switch Report command  is used to advertise the status of a device with On/Off or Enable/Disable\n *  capability.\n *\n *  Action: Log info message.\n *\n *  cmd attributes:\n *    Short  value  0xFF for on, 0x00 for off\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {\n    logger(\"zwaveEvent(): Switch Binary Report received: ${cmd}\",\"info\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SWITCH_ALL (0x27) : SWITCH_ALL_REPORT (0x03) )\n *\n *  The All Switch Report Command is used to report if the device is included or excluded from the all on/all off\n *  functionality.\n *\n *  Action: Log an info message.\n *\n *  cmd attributes:\n *    Short    mode\n *      0   = MODE_EXCLUDED_FROM_THE_ALL_ON_ALL_OFF_FUNCTIONALITY\n *      1   = MODE_EXCLUDED_FROM_THE_ALL_ON_FUNCTIONALITY_BUT_NOT_ALL_OFF\n *      2   = MODE_EXCLUDED_FROM_THE_ALL_OFF_FUNCTIONALITY_BUT_NOT_ALL_ON\n *      255 = MODE_INCLUDED_IN_THE_ALL_ON_ALL_OFF_FUNCTIONALITY\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.switchallv1.SwitchAllReport cmd) {\n    logger(\"zwaveEvent(): Switch All Report received: ${cmd}\",\"trace\")\n\n    def msg = \"\"\n    switch (cmd.mode) {\n            case 0:\n                msg = \"Device is excluded from the all on/all off functionality.\"\n                break\n\n            case 1:\n                msg = \"Device is excluded from the all on functionality but not all off.\"\n                break\n\n            case 2:\n                msg = \"Device is excluded from the all off functionality but not all on.\"\n                break\n\n            default:\n                msg = \"Device is included in the all on/all off functionality.\"\n                break\n    }\n    logger(\"Switch All Mode: ${cmd.mode}: ${msg}\",\"info\")\n\n    // Cache in GeneralMd:\n    state.zwtGeneralMd.switchAllModeId = cmd.mode\n    state.zwtGeneralMd.switchAllModeDesc = msg\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SENSOR_MULTILEVEL_V5 (0x31) : SENSOR_MULTILEVEL_REPORT_V5 (0x05) )\n *\n *  The Multilevel Sensor Report Command is used by a multilevel sensor to advertise a sensor reading.\n *\n *  Action: Cache SensorType and log an info message.\n *\n *  Note: SmartThings does not yet have capabilities corresponding to all possible sensor types, therefore\n *  some of the event types raised below are non-standard.\n *\n *  cmd attributes:\n *    Short         precision           Indicates the number of decimals.\n *                                      E.g. The decimal value 1025 with precision 2 is therefore equal to 10.25.\n *    Short         scale               Indicates what unit the sensor uses.\n *    BigDecimal    scaledSensorValue   Sensor value as a double.\n *    Short         sensorType          Sensor Type (8 bits).\n *    List<Short>   sensorValue         Sensor value as an array of bytes.\n *    Short         size                Indicates the number of bytes used for the sensor value.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {\n    logger(\"zwaveEvent(): SensorMultilevelReport received: ${cmd}\",\"trace\")\n\n    def map = [ sensorType: cmd.sensorType, scale: cmd.scale, value: cmd.scaledSensorValue.toString() ]\n\n    // Sensor Types up to V4 only, there are further sensor types up to V10 defined.\n    switch (cmd.sensorType) {\n        case 1:  // Air Temperature (V1)\n            map.name = \"temperature\"\n            map.unit = (cmd.scale == 1) ? \"F\" : \"C\"\n            break\n\n        case 2:  // General Purpose (V1)\n            map.name = \"value\"\n            map.unit = (cmd.scale == 1) ? \"\" : \"%\"\n            break\n\n        case 3:  // Luminance (V1)\n            map.name = \"illuminance\"\n            map.unit = (cmd.scale == 1) ? \"lux\" : \"%\"\n            break\n\n        case 4:  // Power (V2)\n            map.name = \"power\"\n            map.unit = (cmd.scale == 1) ? \"Btu/h\" : \"W\"\n            break\n\n        case 5:  // Humidity (V2)\n            map.name = \"humidity\"\n            map.unit = (cmd.scale == 1) ? \"g/m^3\" : \"%\"\n            break\n\n        case 6:  // Velocity (V2)\n            map.name = \"velocity\"\n            map.unit = (cmd.scale == 1) ? \"mph\" : \"m/s\"\n            break\n\n        case 7:  // Direction (V2)\n            map.name = \"direction\"\n            map.unit = \"\"\n            break\n\n        case 8:  // Atmospheric Pressure (V2)\n        case 9:  // Barometric Pressure (V2)\n            map.name = \"pressure\"\n            map.unit = (cmd.scale == 1) ? \"inHg\" : \"kPa\"\n            break\n\n        case 0xA:  // Solar Radiation (V2)\n            map.name = \"radiation\"\n            map.unit = \"W/m^3\"\n            break\n\n        case 0xB:  // Dew Point (V2)\n            map.name = \"dewPoint\"\n            map.unit = (cmd.scale == 1) ? \"F\" : \"C\"\n            break\n\n        case 0xC:  // Rain Rate (V2)\n            map.name = \"rainRate\"\n            map.unit = (cmd.scale == 1) ? \"in/h\" : \"mm/h\"\n            break\n\n        case 0xD:  // Tide Level (V2)\n            map.name = \"tideLevel\"\n            map.unit = (cmd.scale == 1) ? \"ft\" : \"m\"\n            break\n\n        case 0xE:  // Weight (V3)\n            map.name = \"weight\"\n            map.unit = (cmd.scale == 1) ? \"lbs\" : \"kg\"\n            break\n\n        case 0xF:  // Voltage (V3)\n            map.name = \"voltage\"\n            map.unit = (cmd.scale == 1) ? \"mV\" : \"V\"\n            break\n\n        case 0x10:  // Current (V3)\n            map.name = \"current\"\n            map.unit = (cmd.scale == 1) ? \"mA\" : \"A\"\n            break\n\n        case 0x11:  // Carbon Dioxide Level (V3)\n            map.name = \"carbonDioxide\"\n            map.unit = \"ppm\"\n            break\n\n        case 0x12:  // Air Flow (V3)\n            map.name = \"fluidFlow\"\n            map.unit = (cmd.scale == 1) ? \"cfm\" : \"m^3/h\"\n            break\n\n        case 0x13:  // Tank Capacity (V3)\n            map.name = \"fluidVolume\"\n            map.unit = (cmd.scale == 0) ? \"ltr\" : (cmd.scale == 1) ? \"m^3\" : \"gal\"\n            break\n\n        case 0x14:  // Distance (V3)\n            map.name = \"distance\"\n            map.unit = (cmd.scale == 0) ? \"m\" : (cmd.scale == 1) ? \"cm\" : \"ft\"\n            break\n\n        default:\n            logger(\"zwaveEvent(): SensorMultilevelReport with unhandled sensorType: ${cmd}\",\"warn\")\n            map.name = \"unknown\"\n            map.unit = \"unknown\"\n            break\n    }\n\n    logger(\"New multilevel sensor report: Name: ${map.name}, Value: ${map.value}, Unit: ${map.unit}\",\"info\")\n\n    // Update meta-data cache:\n    if (state.zwtSensorMultilevelReportsMd?.find( { it.sensorType == map.sensorType } )) { // Known SensorMultilevelReport type, so update attributes.\n        state.zwtSensorMultilevelReportsMd?.collect {\n            if (it.sensorType == map.sensorType) {\n                it.scale = map.scale\n                it.name = map.name\n                it.unit = map.unit\n                it.lastValue = map.value\n            }\n        }\n    }\n    else { // New SensorMultilevelReport type:\n        logger(\"zwaveEvent(): New SensorMultilevelReport type discovered.\",\"debug\")\n        state.zwtSensorMultilevelReportsMd << [\n                sensorType: map.sensorType,\n                scale: map.scale,\n                name: map.name,\n                unit: map.unit,\n                lastValue: map.value\n        ]\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_METER_V3 (0x32) : METER_REPORT_V3 (0x02) )\n *\n *  The Meter Report Command is used to advertise a meter reading.\n *\n *  Action: Cache meter report in state.zwtMeterReportsMd, and log info message.\n *\n *  cmd attributes:\n *    Integer        deltaTime                   Time in seconds since last report.\n *    Short          meterType                   Specifies the type of metering device.\n *      0x00 = Unknown\n *      0x01 = Electric meter\n *      0x02 = Gas meter\n *      0x03 = Water meter\n *    List<Short>    meterValue                  Meter value as an array of bytes.\n *    Double         scaledMeterValue            Meter value as a double.\n *    List<Short>    previousMeterValue          Previous meter value as an array of bytes.\n *    Double         scaledPreviousMeterValue    Previous meter value as a double.\n *    Short          size                        The size of the array for the meterValue and previousMeterValue.\n *    Short          scale                       Indicates what unit the sensor uses (dependent on meterType).\n *    Short          precision                   The decimal precision of the values.\n *    Short          rateType                    Specifies if it is import or export values to be read.\n *      0x01 = Import (consumed)\n *      0x02 = Export (produced)\n *    Boolean        scale2                      ???\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {\n    logger(\"zwaveEvent(): Meter Report received: ${cmd}\",\"trace\")\n\n    def result = []\n    def map = [meterType: cmd.meterType, scale: cmd.scale, value: cmd.scaledMeterValue]\n\n    switch (cmd.meterType) {\n        case 1:  // Electric meter:\n            map.meterTypeName = 'Electric'\n            switch (cmd.scale) {\n                case 0:  // Accumulated Energy (kWh):\n                    map.scaleName = 'Accumulated Energy'\n                    map.unit = 'kWh'\n                    break\n\n                case 1:  // Accumulated Energy (kVAh):\n                    map.scaleName = 'Accumulated Energy'\n                    map.unit = 'kVAh'\n                    break\n\n                case 2:  // Instantaneous Power (Watts):\n                    map.scaleName = 'Instantaneous Power'\n                    map.unit = 'W'\n                    break\n\n                case 3:  // Accumulated Pulse Count:\n                    map.scaleName = 'Accumulated Electricity Pulse Count'\n                    map.unit = ''\n                    break\n\n                case 4:  // Instantaneous Voltage (Volts):\n                    map.scaleName = 'Instantaneous Voltage'\n                    map.unit = 'V'\n                    break\n\n                 case 5:  // Instantaneous Current (Amps):\n                    map.scaleName = 'Instantaneous Current'\n                    map.unit = 'A'\n                    break\n\n                 case 6:  // Instantaneous Power Factor:\n                    map.scaleName = 'Instantaneous Power Factor'\n                    map.unit = ''\n                    break\n\n                default:\n                    map.scaleName = 'Unknown'\n                    map.unit = 'Unknown'\n                    break\n            }\n            break\n\n        case 2:  // Gas meter:\n            map.meterTypeName = 'Gas'\n            switch (cmd.scale) {\n                case 0:  // Accumulated Gas Volume (m^3):\n                    map.scaleName = 'Accumulated Gas Volume'\n                    map.unit = 'm^3'\n                    break\n\n                case 1:  // Accumulated Gas Volume (ft^3):\n                    map.scaleName = 'Accumulated Gas Volume'\n                    map.unit = 'ft^3'\n                    break\n\n                case 3:  // Accumulated Pulse Count:\n                    map.scaleName = 'Accumulated Gas Pulse Count'\n                    map.unit = ''\n                    break\n\n                default:\n                    map.scaleName = 'Unknown'\n                    map.unit = 'Unknown'\n                    break\n            }\n            break\n\n        case 3:  // Water meter:\n            map.meterTypeName = 'Water'\n            switch (cmd.scale) {\n                case 0:  // Accumulated Water Volume (m^3):\n                    map.scaleName = 'Accumulated Water Volume'\n                    map.unit = 'm^3'\n                    break\n\n                case 1:  // Accumulated Water Volume (ft^3):\n                    map.scaleName = 'Accumulated Water Volume'\n                    map.unit = 'ft^3'\n                    break\n\n                case 2:  // Accumulated Water Volume (US gallons):\n                    map.scaleName = 'Accumulated Water Volume'\n                    map.unit = 'gal'\n                    break\n\n                case 3:  // Accumulated Pulse Count:\n                    map.scaleName = 'Accumulated Water Pulse Count'\n                    map.unit = ''\n                    break\n\n                default:\n                    map.scaleName = 'Unknown'\n                    map.unit = 'Unknown'\n                    break\n            }\n            break\n\n        default:\n            map.meterTypeName = 'Unknown'\n            map.scaleName = 'Unknown'\n            map.unit = 'Unknown'\n            break\n    }\n\n    logger(\"New meter report: ${map.scaleName}: ${map.value} ${map.unit}\", (map.scaleName == 'Unknown') ? \"warn\" : \"info\")\n\n    // Update meta-data cache:\n    if (state.zwtMeterReportsMd?.find( { it.meterType == map.meterType & it.scale == map.scale } )) { // Known MeterReport type, so update attributes.\n        state.zwtMeterReportsMd?.collect {\n            if (it.meterType == map.meterType & it.scale == map.scale) {\n                it.meterTypeName = map.meterTypeName\n                it.scaleName = map.scaleName\n                it.unit = map.unit\n                it.lastValue = map.value\n            }\n        }\n    }\n    else { // New MeterReport type:\n        logger(\"zwaveEvent(): New MeterReport type discovered.\",\"debug\")\n        state.zwtMeterReportsMd << [\n                meterType: map.meterType,\n                meterTypeName: map.meterTypeName,\n                scale: map.scale,\n                scaleName: map.scaleName,\n                unit: map.unit,\n                lastValue: map.value\n        ]\n    }\n\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_CRC16_ENCAP (0x56) : CRC_16_ENCAP (0x01) )\n *\n *  The CRC-16 Encapsulation Command Class is used to encapsulate a command with an additional CRC-16 checksum\n *  to ensure integrity of the payload. The purpose for this command class is to ensure a higher integrity level\n *  of payloads carrying important data.\n *\n *  Action: Extract the encapsulated command and pass to zwaveEvent().\n *\n *  Note: Validation of the checksum is not necessary as this is performed by the hub.\n *\n *  cmd attributes:\n *    Integer      checksum      Checksum.\n *    Short        command       Command identifier of the embedded command.\n *    Short        commandClass  Command Class identifier of the embedded command.\n *    List<Short>  data          Embedded command data.\n *\n *  Example: Crc16Encap(checksum: 125, command: 2, commandClass: 50, data: [33, 68, 0, 0, 0, 194, 0, 0, 77])\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {\n    logger(\"zwaveEvent(): CRC-16 Encapsulation Command received: ${cmd}\",\"trace\")\n\n    def versions = getCommandClassVersions()\n    def version = versions[cmd.commandClass as Integer]\n    def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)\n    def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)\n    // TO DO: It should be possible to replace the lines above with this line soon...\n    //def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())\n    if (!encapsulatedCommand) {\n        logger(\"zwaveEvent(): Could not extract command from ${cmd}\",\"error\")\n    } else {\n        cacheCommandMd(encapsulatedCommand, \"CRC_16_ENCAP\")\n        return zwaveEvent(encapsulatedCommand)\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_ASSOCIATION_GRP_INFO (0x59) : ASSOCIATION_GROUP_NAME_REPORT (0x02) )\n *\n *  The Association Group Name Report command is used to advertise the name of an association group.\n *\n *  Action: Store the group name in state.zwtAssocGroupsMd\n *\n *  cmd attributes:\n *    Short        groupingIdentifier\n *    Short        lengthOfName\n *    List<Short>  name\n *\n *  Example: AssociationGroupNameReport(groupingIdentifier: 1, lengthOfName: 8, name: [76, 105, 102, 101, 108, 105, 110, 101])\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.associationgrpinfov1.AssociationGroupNameReport cmd) {\n    logger(\"zwaveEvent(): Association Group Name Report received: ${cmd}\",\"trace\")\n\n    def name = new String(cmd.name as byte[])\n    logger(\"Association Group #${cmd.groupingIdentifier} has name: ${name}\",\"info\")\n\n    if(state.zwtAssocGroupsMd.find( { it.id == cmd.groupingIdentifier } )) {\n        state.zwtAssocGroupsMd.collect {\n            if (it.id == cmd.groupingIdentifier) {\n                it.name = name\n            }\n        }\n    }\n    else { // Add new group, but don't trigger sync.\n        state.zwtAssocGroupsMd << [id: cmd.groupingIdentifier, name: new String(cmd.name as byte[])]\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MULTI_CHANNEL_V3 (0x60) : MULTI_CHANNEL_CAPABILITY_REPORT_V3 (0x0A) )\n *\n *  The Multi Channel Capability Report command is used to advertise the generic and specific device class of the\n *  End Point and the supported command classes.\n *\n *  Action: Cache meta-data in state.zwtEndpointsMd, and log an info message.\n *\n *  cmd attributes:\n *    List<Short>  commandClass         The command classes implemented by the device for this endpoint.\n *    Boolean      dynamic              True if the endpoint is dynamic.\n *    Short        endPoint             Endpoint ID. (0-127)\n *    Short        genericDeviceClass   The Generic Device Class of the advertised endpoint.\n *    Short        specificDeviceClass  The Specific Device Class of the advertised endpoint.\n *\n *  Example: MultiChannelCapabilityReport(commandClass: [37, 50], dynamic: false, endPoint: 1,\n *   genericDeviceClass: 16, specificDeviceClass: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCapabilityReport cmd) {\n    logger(\"zwaveEvent(): Multi Channel Capability Report received: ${cmd}\",\"info\")\n\n    // Add Endpoint to meta-data cache:\n    if (state.zwtEndpointsMd?.find( { it.id == cmd.endPoint } )) { // Known endpoint:\n        state.zwtEndpointsMd.collect {\n            if (it.id == cmd.endPoint) {\n                it.dynamic = cmd.dynamic\n                it.genericDeviceClass = cmd.genericDeviceClass\n                it.specificDeviceClass = cmd.specificDeviceClass\n                it.commandClasses = cmd.commandClass\n            }\n        }\n    }\n    else { // New Endpoint:\n        logger(\"zwaveEvent(): New endpoint discovered.\",\"debug\")\n        state.zwtEndpointsMd << [\n            id: cmd.endPoint,\n            dynamic: cmd.dynamic,\n            genericDeviceClass: cmd.genericDeviceClass,\n            specificDeviceClass: cmd.specificDeviceClass,\n            commandClasses: cmd.commandClass\n        ]\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MULTI_CHANNEL_V3 (0x60) : MULTI_CHANNEL_CMD_ENCAP_V3 (0x0D) )\n *\n *  The Multi Channel Command Encapsulation command is used to encapsulate commands. Any command supported by\n *  a Multi Channel End Point may be encapsulated using this command.\n *\n *  Action: Extract the encapsulated command and pass to the appropriate zwaveEvent() handler.\n *\n *  cmd attributes:\n *    Boolean      bitAddress           Set to true if multicast addressing is used.\n *    Short        command              Command identifier of the embedded command.\n *    Short        commandClass         Command Class identifier of the embedded command.\n *    Short        destinationEndPoint  Destination End Point.\n *    List<Short>  parameter            Carries the parameter(s) of the embedded command.\n *    Short        sourceEndPoint       Source End Point.\n *\n *  Example: MultiChannelCmdEncap(bitAddress: false, command: 1, commandClass: 32, destinationEndPoint: 0,\n *            parameter: [0], sourceEndPoint: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {\n    logger(\"zwaveEvent(): Multi Channel Command Encapsulation command received: ${cmd}\",\"info\")\n\n    // Add Endpoint to meta-data cache:\n    if (!state.zwtEndpointsMd?.find( { it.id == cmd.sourceEndPoint } )) { // New Endpoint:\n        logger(\"zwaveEvent(): New endpoint discovered.\",\"debug\")\n        state.zwtEndpointsMd << [id: cmd.sourceEndPoint]\n        sendCommands([zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: cmd.sourceEndPoint)])\n    }\n\n    def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())\n    if (!encapsulatedCommand) {\n        logger(\"zwaveEvent(): Could not extract command from ${cmd}\",\"error\")\n    } else {\n        cacheCommandMd(encapsulatedCommand, \"MULTI_CHANNEL_CMD_ENCAP\", cmd.sourceEndPoint, cmd.destinationEndPoint)\n        return zwaveEvent(encapsulatedCommand)\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_CONFIGURATION_V2 (0x70) : CONFIGURATION_REPORT_V2 (0x03) )\n *\n *  The Configuration Report Command is used to advertise the actual value of the advertised parameter.\n *\n *  Action: Store the value in the parameter cache, update syncPending, and log an info message.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  cmd attributes:\n *    List<Short>  configurationValue  Value of parameter (byte array).\n *    Short        parameterNumber     Parameter ID.\n *    Short        size                Size of parameter's value (bytes).\n *\n *  Example: ConfigurationReport(configurationValue: [10], parameterNumber: 0, reserved11: 0,\n *            scaledConfigurationValue: 10, size: 1)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {\n    logger(\"zwaveEvent(): Configuration Report received: ${cmd}\",\"trace\")\n\n    logger(\"Parameter #${cmd.parameterNumber}: Size: ${cmd.size}, Value: ${cmd.scaledConfigurationValue}\",\"info\")\n\n    if (state.zwtParamsMd.find( { it.id == cmd.parameterNumber } )) { // Parameter is already known, so update attributes.\n        state.zwtParamsMd.collect {\n            if (it.id == cmd.parameterNumber) {\n                it.scaledConfigurationValue = cmd.scaledConfigurationValue\n                it.size = cmd.size\n            }\n        }\n    }\n    else { // new parameter\n        logger(\"zwaveEvent(): New parameter discovered.\",\"debug\")\n        state.zwtParamsMd << [id: cmd.parameterNumber, scaledConfigurationValue: cmd.scaledConfigurationValue, size: cmd.size]\n        // Trigger sync() again if this is the target parameter:\n        if ( cmd.parameterNumber == state.zwtParamTarget?.id ) { sync() }\n    }\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_NOTIFICATION_V3 (0x71) : NOTIFICATION_REPORT_V3 (0x05) )\n *\n *  The Notification Report Command is used to advertise notification information.\n *\n *  Action: Log info message.\n *\n *  cmd attributes:\n *    Short        event                  Event Type (see code below).\n *    List<Short>  eventParameter         Event Parameter(s) (depends on Event type).\n *    Short        eventParametersLength  Length of eventParameter.\n *    Short        notificationStatus     The notification reporting status of the device (depends on push or pull model).\n *    Short        notificationType       Notification Type (see code below).\n *    Boolean      sequence\n *    Short        v1AlarmLevel           Legacy Alarm Level from Alarm CC V1.\n *    Short        v1AlarmType            Legacy Alarm Type from Alarm CC V1.\n *    Short        zensorNetSourceNodeId  Source node ID\n *\n *  Example: NotificationReport(event: 8, eventParameter: [], eventParametersLength: 0, notificationStatus: 255,\n *    notificationType: 8, reserved61: 0, sequence: false, v1AlarmLevel: 0, v1AlarmType: 0, zensorNetSourceNodeId: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {\n    logger(\"zwaveEvent(): Notification Report received: ${cmd}\",\"info\")\n\n    def map = [\n        notificationType: cmd.notificationType,\n        notificationTypeName: \"Unknown\",\n        event: cmd.event,\n        eventName: \"Unknown\"\n    ]\n\n    switch (cmd.notificationType) {\n        case 1:  // Smoke Alarm:\n            map.notificationTypeName = \"Smoke Alarm\"\n            break\n        case 2:  // CO Alarm:\n            map.notificationTypeName = \"CO Alarm\"\n            break\n        case 3:  // CO2 Alarm:\n            map.notificationTypeName = \"CO2 Alarm\"\n            break\n\n        case 4:  // Heat Alarm:\n            map.notificationTypeName = \"Heat Alarm\"\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                    map.eventName = \"Previous Events cleared\"\n                    break\n\n                case 1:  // Overheat detected:\n                case 2:  // Overheat detected, Unknown Location:\n                    map.eventName = \"Overheat Detected\"\n                    break\n\n                case 3:  // Rapid Temperature Rise:\n                case 4:  // Rapid Temperature Rise, Unknown Location:\n                    map.eventName = \"Rapid temperature rise detected\"\n                    break\n\n                case 5:  // Underheat detected:\n                case 6:  // Underheat detected, Unknown Location:\n                     map.eventName = \"Underheat Detected\"\n                     break\n\n                default:\n                    break\n            }\n            break\n\n        case 5:  // Water Alarm:\n            map.notificationTypeName = \"Water Alarm\"\n            break\n\n\n        case 8:  // Power Management:\n            map.notificationTypeName = \"Power Management\"\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                   map.eventName = \"Previous Events cleared\"\n                   break\n\n                case 1:  // Mains Connected:\n                   map.eventName = \"Mains Connected\"\n                   break\n\n                case 2:  // AC Mains Disconnected:\n                   map.eventName = \"AC Mains Disconnected\"\n                   break\n\n                case 3:  // AC Mains Re-connected:\n                   map.eventName = \"AC Mains Re-connected\"\n                   break\n\n                case 4:  // Surge:\n                   map.eventName = \"Surge detected\"\n                   break\n\n                case 5:  // Voltage Drop:\n                    map.eventName = \"Voltage drop detected\"\n                    break\n\n                case 6:  // Over-current:\n                    map.eventName = \"Over-current detected\"\n                    break\n\n                 case 7:  // Over-Voltage:\n                    map.eventName = \"Over-voltage detected\"\n                    break\n\n                 case 8:  // Overload:\n                    map.eventName = \"Overload detected\"\n                    break\n\n                 case 9:  // Load Error:\n                    map.eventName = \"Load Error detected\"\n                    break\n\n                default:\n                    break\n            }\n            break\n\n        case 9:  // System:\n            map.notificationTypeName = \"System Alarm\"\n            switch (cmd.event) {\n                case 0:  // Previous Events cleared:\n                    map.eventName = \"Previous Events cleared\"\n                    break\n\n                case 1:  // Harware Failure:\n                case 3:  // Harware Failure (with manufacturer proprietary failure code):\n                    map.eventName = \"Harware Failure\"\n                    break\n\n                case 2:  // Software Failure:\n                case 4:  // Software Failure (with manufacturer proprietary failure code):\n                    map.eventName = \"Software Failure\"\n                    break\n\n                case 6:  // Tampering:\n                    map.eventName = \"Tampering Detected\"\n                    break\n\n                default:\n                    break\n            }\n            break\n\n        default:\n            logger(\"zwaveEvent(): Notification Report recieved with unhandled notificationType: ${cmd}\",\"warn\")\n            break\n    }\n\n    logger(\"New notification report: NotificationName: ${map.notificationTypeName}, EventName: ${map.eventName}\",\"info\")\n\n    // Update meta-data cache:\n    if (state.zwtNotificationReportsMd?.find( { it.notificationType == map.notificationType & it.event == map.event } )) { // Known NotificationReport type, so update attributes.\n        state.zwtNotificationReportsMd?.collect {\n            if (it.notificationType == map.notificationType & it.event == map.event) {\n                it.notificationTypeName = map.notificationTypeName\n                it.eventName = map.eventName\n            }\n        }\n    }\n    else { // New NotificationReport type:\n        logger(\"zwaveEvent(): New SensorMultilevelReport type discovered.\",\"debug\")\n        state.zwtNotificationReportsMd << [\n                notificationType: map.notificationType,\n                event: map.event,\n                notificationTypeName: map.notificationTypeName,\n                eventName: map.eventName\n        ]\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 (0x72) : MANUFACTURER_SPECIFIC_REPORT_V2 (0x05) )\n *\n *  Manufacturer-Specific Reports are used to advertise manufacturer-specific information, such as product number\n *  and serial number.\n *\n *  Action: Log info message.\n *\n *  Example: ManufacturerSpecificReport(manufacturerId: 153, manufacturerName: GreenWave Reality Inc.,\n *   productId: 2, productTypeId: 2)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {\n    logger(\"zwaveEvent(): Manufacturer-Specific Report received: ${cmd}\",\"trace\")\n\n    // Display as hex strings:\n    def manufacturerIdDisp = String.format(\"%04X\",cmd.manufacturerId)\n    def productIdDisp = String.format(\"%04X\",cmd.productId)\n    def productTypeIdDisp = String.format(\"%04X\",cmd.productTypeId)\n\n    logger(\"Manufacturer-Specific Report: Manufacturer ID: ${manufacturerIdDisp}, Manufacturer Name: ${cmd.manufacturerName}\" +\n    \", Product Type ID: ${productTypeIdDisp}, Product ID: ${productIdDisp}\",\"info\")\n\n    state.zwtGeneralMd.manufacturerId = manufacturerIdDisp\n    state.zwtGeneralMd.manufacturerName = manufacturerName\n    state.zwtGeneralMd.productTypeId = productTypeIdDisp\n    state.zwtGeneralMd.productId = productIdDisp\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_POWERLEVEL (0x73) : POWERLEVEL_REPORT (0x03) )\n *\n *  The Powerlevel Report is used to advertise the current RF transmit power of the device.\n *\n *  Action: Log an info message.\n *\n *  cmd attributes:\n *    Short  powerLevel  The current power level indicator value in effect on the node\n *    Short  timeout     The time in seconds the node has at Power level before resetting to normal Power level.\n *\n *  Example: PowerlevelReport(powerLevel: 0, timeout: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.powerlevelv1.PowerlevelReport cmd) {\n    logger(\"zwaveEvent(): Powerlevel Report received: ${cmd}\",\"trace\")\n    def power = (cmd.powerLevel > 0) ? \"minus${cmd.powerLevel}dBm\" : \"NormalPower\"\n    logger(\"Powerlevel Report: Power: ${power}, Timeout: ${cmd.timeout}\",\"info\")\n    state.zwtGeneralMd.powerlevel = power\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_PROTECTION_V2 (0x75) : PROTECTION_REPORT_V2 (0x03) )\n *\n *  The Protection Report is used to report the protection state of a device.\n *  I.e. measures to prevent unintentional control (e.g. by a child).\n *\n *  Action: Log info message.\n *\n *  cmd attributes:\n *    Short  localProtectionState  Local protection state (i.e. physical switches/buttons)\n *    Short  rfProtectionState     RF protection state.\n *\n *  Example: ProtectionReport(localProtectionState: 0, reserved01: 0, reserved11: 0, rfProtectionState: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.protectionv2.ProtectionReport cmd) {\n    logger(\"zwaveEvent(): Protection Report received: ${cmd}\",\"trace\")\n\n    state.zwtGeneralMd.protectionLocalId = cmd.localProtectionState\n    state.zwtGeneralMd.protectionRFId = cmd.rfProtectionState\n\n    def lp, rfp = \"\"\n\n    switch(cmd.localProtectionState)  {\n        case 0:\n            lp = \"Unprotected\"\n            break\n        case 1:\n            lp = \"Protection by sequence\"\n            break\n        case 2:\n            lp = \"No operation possible\"\n            break\n        default:\n            lp = \"Unknwon\"\n            break\n\n    }\n\n    switch(cmd.rfProtectionState)  {\n        case 0:\n            rfp = \"Unprotected\"\n            break\n        case 1:\n            rfp = \"No RF Control\"\n            break\n        case 2:\n            rfp = \"No RF Response\"\n            break\n        default:\n            rfp = \"Unknwon\"\n            break\n    }\n\n    logger(\"Protection Report: Local: ${cmd.localProtectionState} (${lp}), RF: ${cmd.rfProtectionState} (${rfp})\",\"info\")\n\n    state.zwtGeneralMd.protectionLocalDesc = lp\n    state.zwtGeneralMd.protectionRFDesc = rfp\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_FIRMWARE_UPDATE_MD_V2 (0x7A) : FIRMWARE_MD_REPORT_V2 (0x02) )\n *\n *  The Firmware Meta Data Report Command is used to advertise the status of the current firmware in the device.\n *\n *  Action: Log info message.\n *\n *  cmd attributes:\n *    Integer  checksum        Checksum of the firmware image.\n *    Integer  firmwareId      Firware ID (this is not the firmware version).\n *    Integer  manufacturerId  Manufacturer ID.\n *\n *  Example: FirmwareMdReport(checksum: 50874, firmwareId: 274, manufacturerId: 271)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd) {\n    logger(\"zwaveEvent(): Firmware Metadata Report received: ${cmd}\",\"trace\")\n\n    // Display as hex strings:\n    def firmwareIdDisp = String.format(\"%04X\",cmd.firmwareId)\n    def checksumDisp = String.format(\"%04X\",cmd.checksum)\n\n    logger(\"Firmware Metadata Report: Firmware ID: ${firmwareIdDisp}, Checksum: ${checksumDisp}\",\"info\")\n    state.zwtGeneralMd.firmwareId = firmwareIdDisp\n    state.zwtGeneralMd.firmwareChecksum = checksumDisp\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_ASSOCIATION_V2 (0x85) : ASSOCIATION_REPORT_V2 (0x03) )\n *\n *  The Association Report command is used to advertise the current destination nodes of a given association group.\n *\n *  Action: Cache value and log info message only.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  Example: AssociationReport(groupingIdentifier: 1, maxNodesSupported: 1, nodeId: [1], reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {\n    logger(\"zwaveEvent(): Association Report received: ${cmd}\",\"trace\")\n\n    logger(\"Association Group #${cmd.groupingIdentifier} contains nodes: ${toHexString(cmd.nodeId)} (hexadecimal format)\",\"info\")\n\n    if (state.zwtAssocGroupsMd.find( { it.id == cmd.groupingIdentifier } )) { // Group is already known, so update attributes.\n        state.zwtAssocGroupsMd.collect {\n            if (it.id == cmd.groupingIdentifier) {\n                it.maxNodesSupported = cmd.maxNodesSupported\n                it.nodes = cmd.nodeId\n            }\n        }\n    }\n    else { // New group:\n        logger(\"zwaveEvent(): New association group discovered.\",\"debug\")\n        state.zwtAssocGroupsMd << [id: cmd.groupingIdentifier, maxNodesSupported: cmd.maxNodesSupported, nodes: cmd.nodeId]\n        // Trigger sync() again if this is the target association group:\n        if ( cmd.groupingIdentifier == state.zwtAssocGroupTarget?.id ) { sync() }\n    }\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_VERSION (0x86) : VERSION_REPORT (0x12) )\n *\n *  The Version Report Command is used to advertise the library type, protocol version, and application version.\n\n *  Action: Log an info message.\n *\n *  cmd attributes:\n *    Short  applicationSubVersion\n *    Short  applicationVersion\n *    Short  zWaveLibraryType\n *    Short  zWaveProtocolSubVersion\n *    Short  zWaveProtocolVersion\n *\n *  Example: VersionReport(applicationSubVersion: 4, applicationVersion: 3, zWaveLibraryType: 3,\n *   zWaveProtocolSubVersion: 5, zWaveProtocolVersion: 4)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {\n    logger(\"zwaveEvent(): Version Report received: ${cmd}\",\"trace\")\n\n    def zWaveLibraryTypeDisp  = String.format(\"%02X\",cmd.zWaveLibraryType)\n    def zWaveLibraryTypeDesc  = \"\"\n    switch(cmd.zWaveLibraryType) {\n        case 1:\n            zWaveLibraryTypeDesc = \"Static Controller\"\n            break\n\n        case 2:\n            zWaveLibraryTypeDesc = \"Controller\"\n            break\n\n        case 3:\n            zWaveLibraryTypeDesc = \"Enhanced Slave\"\n            break\n\n        case 4:\n            zWaveLibraryTypeDesc = \"Slave\"\n            break\n\n        case 5:\n            zWaveLibraryTypeDesc = \"Installer\"\n            break\n\n        case 6:\n            zWaveLibraryTypeDesc = \"Routing Slave\"\n            break\n\n        case 7:\n            zWaveLibraryTypeDesc = \"Bridge Controller\"\n            break\n\n        case 8:\n            zWaveLibraryTypeDesc = \"Device Under Test (DUT)\"\n            break\n\n        case 0x0A:\n            zWaveLibraryTypeDesc = \"AV Remote\"\n            break\n\n        case 0x0B:\n            zWaveLibraryTypeDesc = \"AV Device\"\n            break\n\n        default:\n            zWaveLibraryTypeDesc = \"N/A\"\n    }\n\n    def applicationVersionDisp = String.format(\"%d.%02d\",cmd.applicationVersion,cmd.applicationSubVersion)\n    def zWaveProtocolVersionDisp = String.format(\"%d.%02d\",cmd.zWaveProtocolVersion,cmd.zWaveProtocolSubVersion)\n\n    logger(\"Version Report: Application Version: ${applicationVersionDisp}, \" +\n           \"Z-Wave Protocol Version: ${zWaveProtocolVersionDisp}, \" +\n           \"Z-Wave Library Type: ${zWaveLibraryTypeDisp} (${zWaveLibraryTypeDesc})\",\"info\")\n\n    // Store in GeneralMd cache:\n    state.zwtGeneralMd.applicationVersion = applicationVersionDisp\n    state.zwtGeneralMd.zWaveProtocolVersion = zWaveProtocolVersionDisp\n    state.zwtGeneralMd.zWaveLibraryType = \"${zWaveLibraryTypeDisp} (${zWaveLibraryTypeDesc})\"\n\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_INDICATOR (0x87) : INDICATOR_REPORT (0x03) )\n *\n *  The Indicator Report command is used to advertise the state of an indicator.\n *\n *  Action: Log info message.\n *\n *  cmd attributes:\n *    Short value  Indicator status.\n *      0x00       = Off/Disabled\n *      0x01..0x63 = Indicator Range.\n *      0xFF       = On/Enabled.\n *\n *  Example: IndicatorReport(value: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.indicatorv1.IndicatorReport cmd) {\n    logger(\"zwaveEvent(): Indicator Report received: ${cmd}\",\"info\")\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION_V2 (0x8E) : ASSOCIATION_REPORT_V2 (0x03) )\n *\n *  The Multi-channel Association Report command is used to advertise the current destinations of a given\n *  association group (nodes and endpoints).\n *\n *  Action: Store the destinations in the zwtAssocGroup cache, update syncPending, and log an info message.\n *\n *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this\n *  is not possible due to security restrictions in the SmartThings platform.\n *\n *  Example: MultiChannelAssociationReport(groupingIdentifier: 2, maxNodesSupported: 8, nodeId: [9,0,1,1,2,3],\n *            reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) {\n    logger(\"zwaveEvent(): Multi-Channel Association Report received: ${cmd}\",\"trace\")\n\n    logger(\"Association Group #${cmd.groupingIdentifier} contains destinations: ${toHexString(cmd.nodeId)} (hexadecimal format)\",\"info\")\n\n    if (state.zwtAssocGroupsMd.find( { it.id == cmd.groupingIdentifier } )) { // Group is already known, so update attributes.\n        state.zwtAssocGroupsMd.collect {\n            if (it.id == cmd.groupingIdentifier) {\n                it.nodes = cmd.nodeId\n                if (cmd.maxNodesSupported > 0) { // Assoc Group supports MultiChannel only if maxNodesSupported > 0.\n                    it.multiChannel = true\n                    it.maxNodesSupported = cmd.maxNodesSupported\n                }\n            }\n        }\n    }\n    else { // New group:\n        logger(\"zwaveEvent(): New association group discovered.\",\"debug\")\n        def newAssocGroup = [id: cmd.groupingIdentifier, nodes: cmd.nodeId]\n        if (cmd.maxNodesSupported > 0) {\n            newAssocGroup.multiChannel = true\n            newAssocGroup.maxNodesSupported = cmd.maxNodesSupported\n        }\n        state.zwtAssocGroupsMd << newAssocGroup\n        // Trigger sync() again if this is the target association group:\n        if ( cmd.groupingIdentifier == state.zwtAssocGroupTarget?.id ) { sync() }\n    }\n\n    updateSyncPending()\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SECURITY (0x98) : SECURITY_MESSAGE_ENCAPSULATION (0x81) )\n *\n *  The Security Message Encapsulation command is used to encapsulate Z-Wave commands using AES-128.\n *\n *  Action: Extract the encapsulated command and pass to the appropriate zwaveEvent() handler.\n *    Set state.useSecurity flag to true.\n *\n *  cmd attributes:\n *    List<Short> commandByte         Parameters of the encapsulated command.\n *    Short   commandClassIdentifier  Command Class ID of the encapsulated command.\n *    Short   commandIdentifier       Command ID of the encapsulated command.\n *    Boolean secondFrame             Indicates if first or second frame.\n *    Short   sequenceCounter\n *    Boolean sequenced               True if the command is transmitted using multiple frames.\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {\n    logger(\"zwaveEvent(): Security Encapsulated Command received: ${cmd}\",\"trace\")\n\n    state.useSecurity = true\n\n    def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())\n    if (encapsulatedCommand) {\n        cacheCommandMd(encapsulatedCommand, \"SECURITY_MESSAGE_ENCAPSULATION\")\n        return zwaveEvent(encapsulatedCommand)\n    } else {\n        logger(\"zwaveEvent(): Unable to extract security encapsulated command from: ${cmd}\",\"error\")\n    }\n}\n\n/**\n *  zwaveEvent( COMMAND_CLASS_SECURITY (0x98) : SECURITY_COMMANDS_SUPPORTED_REPORT (0x03) )\n *\n *  The Security Commands Supported Report command advertises which command classes are supported using security\n *  encapsulation.\n *\n *  Action: Log an info message. Set state.useSecurity flag to true.\n *\n *  cmd attributes:\n *    List<Short>  commandClassControl\n *    List<Short>  commandClassSupport\n *    Short        reportsToFollow\n *\n *  Exmaple: SecurityCommandsSupportedReport(commandClassControl: [43],\n *   commandClassSupport: [32, 90, 133, 38, 142, 96, 112, 117, 39], reportsToFollow: 0)\n **/\ndef zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {\n    logger(\"zwaveEvent(): Security Commands Supported Reportreceived: ${cmd}\",\"trace\")\n\n    state.useSecurity = true\n    state.zwtGeneralMd.securityCommandClassSupport = cmd.commandClassSupport.sort()\n    state.zwtGeneralMd.securityCommandClassControl = cmd.commandClassControl.sort()\n\n    logger(\"Command classes supported with security encapsulation: ${toCcNames(state.zwtGeneralMd.securityCommandClassSupport, true)}\",\"info\")\n    logger(\"Command classes supported for CONTROL with security encapsulation: ${toCcNames(state.zwtGeneralMd.securityCommandClassControl, true)}\",\"info\")\n}\n\n/**\n *  zwaveEvent( DEFAULT CATCHALL )\n *\n *  Called for all commands that aren't handled above.\n **/\ndef zwaveEvent(physicalgraph.zwave.Command cmd) {\n    logger(\"zwaveEvent(): No handler for command: ${cmd}\",\"warn\")\n}\n\n/*****************************************************************************************************************\n *  Commands:\n *****************************************************************************************************************/\n\n/**\n *  initialise()\n *\n *  Sets up meta-data caches, parses fingerprint, and determines if the device is using security encapsulation.\n **/\nprivate initialise() {\n    logger(\"initialise()\",\"trace\")\n\n    // Initialise meta-data stores if they don't exist:\n    if (!state.zwtGeneralMd) state.zwtGeneralMd = [:] // Map!\n    if (!state.zwtCommandsMd) state.zwtCommandsMd = []\n    if (!state.zwtAssocGroupsMd) state.zwtAssocGroupsMd = []\n    if (!state.zwtEndpointsMd) state.zwtEndpointsMd = []\n    if (!state.zwtParamsMd) state.zwtParamsMd = []\n    if (!state.zwtMeterReportsMd) state.zwtMeterReportsMd = []\n    if (!state.zwtNotificationReportsMd) state.zwtNotificationReportsMd = []\n    if (!state.zwtSensorMultilevelReportsMd) state.zwtSensorMultilevelReportsMd = []\n\n    // Parse fingerprint for supported command classes:\n    def ccIds = []\n    if (getZwaveInfo()?.cc) {\n        logger(\"Device has new-style fingerprint: ${device.rawDescription}\",\"info\")\n        ccIds = getZwaveInfo()?.cc + getZwaveInfo()?.sec\n    }\n    else {\n        logger(\"Device has legacy fingerprint: ${device.rawDescription}\",\"info\")\n        // Look for hexadecimal numbers (0x##) but remove the first one, which will be deviceID.\n        ccIds = device.rawDescription.findAll(/0x\\p{XDigit}+/)\n        if (ccIds.size() > 0) { ccIds.remove(0) }\n    }\n    ccIds.removeAll([null])\n    state.zwtGeneralMd.commandClassIds = ccIds.sort().collect { Integer.parseInt(it.replace(\"0x\",\"\"),16) } // Parse hex strings to ints.\n    state.zwtGeneralMd.commandClassNames = toCcNames(state.zwtGeneralMd.commandClassIds,true) // Parse Ids to names.\n    logger(\"Supported Command Classes: ${state.zwtGeneralMd.commandClassNames}\",\"info\")\n\n    // Check zwaveInfo to see if device is using security:\n    if (getZwaveInfo()?.zw?.contains(\"s\")) {\n        logger(\"Device is securly paired. Using secure commands.\",\"info\")\n        state.useSecurity = true\n    }\n\n    // Send a secured command, to double-check security.\n    def cmds = []\n    cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.securityV1.securityCommandsSupportedGet()).format()\n\n    sendCommands(cmds,200)\n    state.zwtInitialised = true\n}\n\n/**\n *  scanGeneral()\n *\n *  Scans for common device attributes such as battery/firmware/version etc.\n **/\nprivate scanGeneral() {\n    logger(\"scanGeneral(): Scanning for common device attributes.\",\"info\")\n\n    if (!state.zwtInitialised) { initialise() }\n\n    def cmds = []\n\n    cmds << zwave.batteryV1.batteryGet()\n    cmds << zwave.firmwareUpdateMdV2.firmwareMdGet()\n    cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet()\n    cmds << zwave.powerlevelV1.powerlevelGet()\n    cmds << zwave.protectionV2.protectionGet()\n    cmds << zwave.switchAllV1.switchAllGet()\n    cmds << zwave.versionV1.versionGet()\n    cmds << zwave.wakeUpV1.wakeUpIntervalGet()\n\n    sendCommands(cmds,800)\n}\n\n/**\n *  scanAssocGroups()\n *\n *  Scans for association groups. If a group is already known, it is not scanned again.\n **/\nprivate scanAssocGroups() {\n    logger(\"scanAssocGroups(): Scanning Association Groups.\",\"trace\")\n\n    if (!state.zwtInitialised) { initialise() }\n\n    // Check the device supports ASSOCIATION or MULTI_CHANNEL_ASSOCIATION, warn if it doesn't.\n    if (!state.zwtGeneralMd?.commandClassIds.find( {it == 0x85 || it == 0x8E }) ) {\n        logger(\"sync(): Device does not appear to support ASSOCIATION or MULTI_CHANNEL_ASSOCIATION command classes.\",\"warn\")\n    }\n\n    def cmds = []\n\n    def start = (settings.zwtAssocGroupsScanStart) ? settings.zwtAssocGroupsScanStart.toInteger() : 0\n    def stop = (settings.zwtAssocGroupsScanStop) ? settings.zwtAssocGroupsScanStop.toInteger() : 10\n\n    logger(\"scanAssocGroups(): Scanning Association Groups (#${start} to #${stop}).\",\"info\")\n    (start..stop).each { i ->\n        if (!state.zwtAssocGroupsMd.find( { it.id == i } )) {\n            cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: i)\n            cmds << zwave.associationV2.associationGet(groupingIdentifier: i)\n            cmds << zwave.associationGrpInfoV1.associationGroupNameGet(groupingIdentifier: i)\n        }\n        else if (!state.zwtAssocGroupsMd.find( { it.id == i } )?.name) {\n            cmds << zwave.associationGrpInfoV1.associationGroupNameGet(groupingIdentifier: i)\n        }\n    }\n\n    sendCommands(cmds,800)\n}\n\n/**\n *  scanEndpoints()\n *\n *  Scans for endpoints in multi-channel devices.\n **/\nprivate scanEndpoints() {\n    logger(\"scanEndpoints(): Scanning for Endpoints.\",\"trace\")\n\n    if (!state.zwtInitialised) { initialise() }\n\n    // Check the device supports MULTI_CHANNEL, warn if it doesn't.\n    if (!state.zwtGeneralMd?.commandClassIds.find( {it == 0x60 }) ) {\n        logger(\"sync(): Device does not appear to support MULTI_CHANNEL command classes.\",\"warn\")\n    }\n\n    def cmds = []\n    //cmds << zwave.multiChannelV3.multiChannelEndPointGet()\n    // Returns: MultiChannelEndPointReport(dynamic: false, endPoints: 3, identical: true, res00: 0, res11: false)\n    // Only really useful to tell us if the device is using dynamic endpoints.\n\n    // Using multiChannelEndPointFind(genericDeviceClass: 255, specificDeviceClass: 255) is supposed to return all endpoints\n    //cmds << zwave.multiChannelV3.multiChannelEndPointFind(genericDeviceClass: 255, specificDeviceClass: 255)\n    // However, typically we get back MultiChannelEndPointFindReport(genericDeviceClass: 255, reportsToFollow: 0, specificDeviceClass: 255)\n    // which doesn't list any endpoints, so it's not very useful.\n\n    def start = (settings.zwtEndpointsScanStart) ? settings.zwtEndpointsScanStart.toInteger() : 0\n    def stop = (settings.zwtEndpointsScanStop) ? settings.zwtEndpointsScanStop.toInteger() : 10\n\n    logger(\"scanEndpoints(): Scanning for Endpoints (#${start} to #${stop}).\",\"info\")\n    (start..stop).each {\n        cmds << zwave.multiChannelV3.multiChannelCapabilityGet(endPoint: it)\n    }\n\n    sendCommands(cmds,800)\n}\n\n/**\n *  scanParams()\n *\n *  Scans for device parameters. If a parameter is already known, it is not scanned again.\n **/\nprivate scanParams() {\n    logger(\"scanParams(): Scanning Device Parameters.\",\"trace\")\n\n    if (!state.zwtInitialised) { initialise() }\n\n    // Check the device supports CONFIGURATION, warn if it doesn't.\n    if (!state.zwtGeneralMd?.commandClassIds.find( {it == 0x70}) ) {\n        logger(\"sync(): Device does not appear to support the CONFIGURATION command class.\",\"warn\")\n    }\n\n    def cmds = []\n\n    // Use BulkGet (few devices support this).\n    //cmds << zwave.configurationV2.configurationBulkGet(numberOfParameters: 10, parameterOffset: 1)\n\n    //Try a CONFIGURATION_NAME_GET (there is no class for configurationV3 yet, so have to build raw command:\n    //cmds << \"988100\" + \"700A0001\" // This is COMMAND_CLASS_CONFIGURATION, CONFIGURATION_NAME_GET, Parameter 01 (16-bit).\n\n    def start = (settings.zwtParamsScanStart) ? settings.zwtParamsScanStart.toInteger() : 0\n    def stop = (settings.zwtParamsScanStop) ? settings.zwtParamsScanStop.toInteger() : 20\n\n    logger(\"scanParams(): Scanning Device Parameters (#${start} to #${stop}).\",\"info\")\n    (start..stop).each { i ->\n        if (!state.zwtParamsMd.find( { it.id == i } )) {\n            cmds << zwave.configurationV2.configurationGet(parameterNumber: i)\n        }\n    }\n\n    sendCommands(cmds,800)\n}\n\n/**\n *  scanActuator()\n *\n *  Scans for common actuator attributes, such as switch and lock state.\n **/\nprivate scanActuator() {\n    logger(\"scanActuator(): Scanning for common actuator values.\",\"info\")\n\n    if (!state.zwtInitialised) { initialise() }\n\n    def cmds = []\n\n    cmds << zwave.basicV1.basicGet()\n    cmds << zwave.doorLockV1.doorLockOperationGet()\n    cmds << zwave.indicatorV1.indicatorGet()\n    cmds << zwave.lockV1.lockGet()\n    cmds << zwave.switchBinaryV1.switchBinaryGet()\n    cmds << zwave.switchColorV3.switchColorGet()\n    cmds << zwave.switchMultilevelV2.switchMultilevelGet()\n\n    sendCommands(cmds,800)\n}\n\n/**\n *  scanSensor()\n *\n *  Scans for common sensor attributes, such as meter, sensorBinary, sensorMultilevel.\n *\n *  Note: To save time, only scans using command classes that the device advertises. Plus, only the primary\n *   node is scanned.\n *\n *  To Do: Scan all endpoints of a multi-channel device.\n **/\nprivate scanSensor() {\n    logger(\"scanSensor(): Scanning for common sensor types (this can take several minutes to complete).\",\"info\")\n\n    if (!state.zwtInitialised) { initialise() }\n\n    def cmds = []\n\n    // sensorBinary:\n    if (state.zwtGeneralMd?.commandClassIds.find( {it == 0x30 }) ) {\n        logger(\"scanSensor(): Scanning sensorBinary sensorTypes:\",\"info\")\n        cmds << zwave.sensorBinaryV2.sensorBinarySupportedGetSensor()\n        (0..13).each { sT -> // Scan SensorTypes 0-13 (i.e. all up to V2).\n            cmds << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: sT)\n        }\n    }\n\n    // sensorMultilevel:\n    if (state.zwtGeneralMd?.commandClassIds.find( {it == 0x31 }) ) {\n        logger(\"scanSensor(): Scanning sensorMultilevel sensorTypes:\",\"info\")\n        // These are relatively new and not widely supported:\n        cmds << zwave.sensorMultilevelV5.sensorMultilevelSupportedGetSensor()\n        cmds << zwave.sensorMultilevelV5.sensorMultilevelSupportedGetScale()\n        // So we brute-force scan:\n        (0..31).each { sT -> // Scan SensorTypes 0-31 (i.e. all up to V5).\n            //(0..3).each { s -> // Scan scales 0-3\n                cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: sT)\n            //}\n        }\n    }\n\n    // meter:\n    if (state.zwtGeneralMd?.commandClassIds.find( {it == 0x32 }) ) {\n        logger(\"scanSensor(): Scanning meter scales:\",\"info\")\n        (0..6).each { cmds << zwave.meterV3.meterGet(scale: it) }\n    }\n\n    // alarm/notification:\n    if (state.zwtGeneralMd?.commandClassIds.find( {it == 0x71 }) ) {\n        logger(\"scanSensor(): Scanning alarm/notification:\",\"info\")\n        cmds << zwave.notificationV3.notificationSupportedGet()\n        (0..11).each { nT -> // Scan notificationTypes 0-11.\n            cmds << zwave.notificationV3.notificationGet(notificationType: nT)\n        }\n    }\n\n    sendCommands(cmds,800)\n}\n\n/**\n *  printGeneral()\n *\n *  Outputs general/summary information to the IDE Log.\n **/\nprivate printGeneral() {\n    logger(\"printGeneral(): Printing general info.\",\"trace\")\n\n    def output = \"General Information:\"\n    def pageSize = 10\n    def itemCount = 0\n\n    output += \"\\nDevice Name: ${device?.displayName}\"\n    output += \"\\nRaw Description: ${device?.rawDescription}\"\n    output += \"\\nSupported Command Classes: ${state.zwtGeneralMd?.commandClassNames}\"\n\n    if (state.useSecurity) {\n        output += \"\\nSecurity: Device is paired securely.\"\n        output += \"\\n => Command classes supported with security encapsulation: ${toCcNames(state.zwtGeneralMd?.securityCommandClassSupport, true)}\"\n        output += \"\\n => Command classes supported for CONTROL with security encapsulation: ${toCcNames(state.zwtGeneralMd?.securityCommandClassControl, true)}\"\n    }\n    else {\n        output += \"\\nSecurity: Device is not using security.\"\n    }\n\n    output += \"\\nManufacturer ID: ${state.zwtGeneralMd?.manufacturerId}\"\n    output += \"\\nManufacturer Name: ${state.zwtGeneralMd?.manufacturerName}\"\n    output += \"\\nProduct Type ID: ${state.zwtGeneralMd?.productTypeId}\"\n    output += \"\\nProduct ID: ${state.zwtGeneralMd?.productId}\"\n\n    output += \"\\nFirmware Metadata: Firmware ID: ${state.zwtGeneralMd?.firmwareId}, Checksum: ${state.zwtGeneralMd?.firmwareChecksum}\"\n    output += \"\\nApplication (Firmware) Version: ${state.zwtGeneralMd?.applicationVersion}\"\n    output += \"\\nZ-Wave Protocol Version: ${state.zwtGeneralMd?.zWaveProtocolVersion}\"\n    output += \"\\nZ-Wave Library Type: ${state.zwtGeneralMd?.zWaveLibraryType}\"\n\n    output += \"\\nPowerlevel: ${state.zwtGeneralMd?.powerlevel}\"\n    output += \"\\nProtection Mode: [ Local: ${state.zwtGeneralMd?.protectionLocalId} (${state.zwtGeneralMd?.protectionLocalDesc}), \"\n    output += \"RF: ${state.zwtGeneralMd?.protectionRFId} (${state.zwtGeneralMd?.protectionRFDesc}) ]\"\n    output += \"\\nSwitch_All Mode: ${state.zwtGeneralMd?.switchAllModeId} (${state.zwtGeneralMd?.switchAllModeDesc})\"\n\n    logger(output,\"info\")\n\n    output  = \"Discovery Stats:\"\n    output += \"\\nNumber of association groups discovered: ${state.zwtAssocGroupsMd?.size()} [Print Assoc Groups]\"\n    output += \"\\nNumber of endpoints discovered: ${state.zwtEndpointsMd?.size()} [Print Endpoints]\"\n    output += \"\\nNumber of parameters discovered: ${state.zwtParamsMd?.size()} [Print Parameters]\"\n    output += \"\\nNumber of unique command types received: ${state.zwtCommandsMd?.size()} [Print Commands]\"\n    output += \"\\nNumber of MeterReport types discovered: ${state.zwtMeterReportsMd?.size()} [Print Sensor]\"\n    output += \"\\nNumber of NotificationReport types discovered: ${state.zwtNotificationReportsMd?.size()} [Print Sensor]\"\n    output += \"\\nNumber of SensorMultilevelReport types discovered: ${state.zwtSensorMultilevelReportsMd?.size()} [Print Sensor]\"\n\n    logger(output,\"info\")\n}\n\n/**\n *  printAssocGroups()\n *\n *  Outputs association group information to the IDE Log.\n **/\nprivate printAssocGroups() {\n    logger(\"printAssocGroups(): Printing association groups.\",\"trace\")\n\n    def output = \"\"\n    def pageSize = 10\n    def itemCount = 0\n\n    output = \"Association groups [${state.zwtAssocGroupsMd?.size()}]:\"\n    state.zwtAssocGroupsMd?.sort( { it.id } ).each {\n        def assocGroup = it.clone() // Make copy (don't want to turn the orginal to strings)\n        if (assocGroup.nodes) { assocGroup.nodes = toHexString(assocGroup.nodes) }\n        output += \"\\nAssociation Group #${assocGroup.id}: ${assocGroup.sort()}\"\n        itemCount++\n        if (itemCount >= pageSize) {\n            logger(output,\"info\")\n            output = \"\"\n            itemCount = 0\n        }\n    }\n    logger(output,\"info\")\n\n}\n\n/**\n *  printEndpoints()\n *\n *  Outputs endpoint information to the IDE Log.\n **/\nprivate printEndpoints() {\n    logger(\"printEndpoints(): Printing endpoints.\",\"trace\")\n\n    def output = \"\"\n    def pageSize = 5\n    def itemCount = 0\n\n    output = \"Endpoints [${state.zwtEndpointsMd?.size()}]:\"\n    state.zwtEndpointsMd?.sort( { it.id } ).each {\n        def eP = it.clone() // Make copy (don't want to turn the orginal to strings)\n        if (eP.commandClasses) {\n            eP.commandClassNames = toCcNames(eP.commandClasses,true)\n            eP.commandClasses = toHexString(eP.commandClasses,2,true)\n        }\n        output += \"\\nEndpoint #${eP.id}: [id: ${eP.id}, dynamic: ${eP.dynamic}, \"\n        output += \"genericDeviceClass: ${eP.genericDeviceClass}, specificDeviceClass: ${eP.specificDeviceClass}, \"\n        output += \"Supported Commands: ${eP.commandClassNames} ]\"\n        itemCount++\n        if (itemCount >= pageSize) {\n            logger(output,\"info\")\n            output = \"\"\n            itemCount = 0\n        }\n    }\n    logger(output,\"info\")\n}\n\n/**\n *  printParams()\n *\n *  Outputs parameter information to the IDE Log.\n **/\nprivate printParams() {\n    logger(\"printParams(): Printing parameters.\",\"trace\")\n\n    def output = \"\"\n    def pageSize = 20\n    def itemCount = 0\n\n    output = \"Parameters [${state.zwtParamsMd?.size()}]:\"\n    state.zwtParamsMd?.sort( { it.id } ).each {\n        output += \"\\nParameter #${it.id}: ${it.sort()}\"\n        itemCount++\n        if (itemCount >= pageSize) {\n            logger(output,\"info\")\n            output = \"\"\n            itemCount = 0\n        }\n    }\n    logger(output,\"info\")\n}\n\n/**\n *  printActuator()\n *\n *  Outputs actuator information to the IDE Log.\n *  No meta-data about actuator commands is currently stored, so just call printCommands().\n **/\nprivate printActuator() {\n    logger(\"printSensor(): Printing actuator information.\",\"trace\")\n    printCommands()\n}\n\n/**\n *  printSensor()\n *\n *  Outputs sensor information to the IDE Log.\n **/\nprivate printSensor() {\n    logger(\"printSensor(): Printing sensor information.\",\"trace\")\n\n    def output = \"\"\n    def pageSize = 10\n    def itemCount = 0\n\n    // SensorMultilevelReports:\n    output = \"SensorMultilevelReport types [${state.zwtSensorMultilevelReportsMd?.size()}]:\"\n    state.zwtSensorMultilevelReportsMd?.sort( { it.sensorType } ).each {\n        output += \"\\nSensorMultilevelReport: ${it.sort()}\"\n        itemCount++\n        if (itemCount >= pageSize) {\n            logger(output,\"info\")\n            output = \"\"\n            itemCount = 0\n        }\n    }\n    logger(output,\"info\")\n    output = \"\"\n    itemCount = 0\n\n    // MeterReports:\n    output = \"MeterReport types [${state.zwtMeterReportsMd?.size()}]:\"\n    state.zwtMeterReportsMd?.sort( { a,b -> a.meterType <=> b.meterType ?: a.scale <=> b.scale } ).each { // Sort by meterType, then scale.\n        output += \"\\nMeterReport: ${it.sort()}\"\n        itemCount++\n        if (itemCount >= pageSize) {\n            logger(output,\"info\")\n            output = \"\"\n            itemCount = 0\n        }\n    }\n    logger(output,\"info\")\n    output = \"\"\n    itemCount = 0\n\n    // alarm/notification:\n    output = \"NotificationReport types [${state.zwtNotificationReportsMd?.size()}]:\"\n    state.zwtNotificationReportsMd?.sort( { a,b -> a.notificationType <=> b.notificationType ?: a.event <=> b.event } ).each { // Sort by notificationType, then event.\n        output += \"\\nNotificationReport: ${it}\"\n        itemCount++\n        if (itemCount >= pageSize) {\n            logger(output,\"info\")\n            output = \"\"\n            itemCount = 0\n        }\n    }\n    logger(output,\"info\")\n}\n\n/**\n *  printCommands()\n *\n *  Outputs information about all unique command types received to the IDE Log.\n **/\nprivate printCommands() {\n    logger(\"printCommands(): Printing commands.\",\"trace\")\n\n    def output = \"\"\n    def pageSize = 5\n    def itemCount = 0\n\n    output = \"Command types [${state.zwtCommandsMd?.size()}]:\"\n    state.zwtCommandsMd?.sort( { a,b -> a.commandClassId <=> b.commandClassId ?: a.commandId <=> b.commandId } ).each {\n        def command = it.clone() // Make copy (don't want to turn the orginal to strings)\n        if (command.commandClassId) { command.commandClassId = toHexString(command.commandClassId,2,true) }\n        if (command.commandId) { command.commandId = toHexString(command.commandId,2,true) }\n\n        output += \"\\nCommand: [commandClassId: ${command.commandClassId}, commandClassName: ${command.commandClassName}, \" +\n        \"commandID: ${command.commandId}, description: ${command.description}]\\n\" +\n        \" => Example: ${command.parsedCmd}\"\n        if (command.sourceEndpoint) { output += \"\\nsourceEndpoint: ${command.sourceEndpoint}, destinationEndpoint ${command.destinationEndpoint}\"}\n        itemCount++\n        if (itemCount >= pageSize) {\n            logger(output,\"info\")\n            output = \"\"\n            itemCount = 0\n        }\n    }\n    logger(output,\"info\")\n}\n\n/**\n *  cleanUp()\n *\n *  Cleans up the device handler state, ready for reinstatement of the original device handler.\n **/\nprivate cleanUp() {\n    logger(\"cleanUp(): Cleaning up device state.\",\"info\")\n\n    state.remove(\"zwtInitialised\")\n    state.remove(\"zwtGeneralMd\")\n    state.remove(\"zwtCommandsMd\")\n    state.remove(\"zwtAssocGroupsMd\")\n    state.remove(\"zwtEndpointsMd\")\n    state.remove(\"zwtParamsMd\")\n    state.remove(\"zwtMeterReportsMd\")\n    state.remove(\"zwtNotificationReportsMd\")\n    state.remove(\"zwtSensorMultilevelReportsMd\")\n    state.remove(\"zwtAssocGroupTarget\")\n    state.remove(\"zwtParamTarget\")\n\n    device?.updateSetting(\"zwtLoggingLevelIDE\", null)\n    device?.updateSetting(\"zwtAssocGroupsScanStart\", null)\n    device?.updateSetting(\"zwtAssocGroupsScanStop\", null)\n    device?.updateSetting(\"zwtEndpointsScanStart\", null)\n    device?.updateSetting(\"zwtEndpointsScanStop\", null)\n    device?.updateSetting(\"zwtParamsScanStart\", null)\n    device?.updateSetting(\"zwtParamsScanStop\", null)\n    device?.updateSetting(\"zwtAssocGroupId\", null)\n    device?.updateSetting(\"zwtAssocGroupMembers\", null)\n    device?.updateSetting(\"zwtAssocGroupCc\", null)\n    device?.updateSetting(\"zwtParamId\", null)\n    device?.updateSetting(\"zwtParamValue\", null)\n    device?.updateSetting(\"zwtProtectLocal\", null)\n    device?.updateSetting(\"zwtProtectRF\", null)\n    device?.updateSetting(\"zwtSwitchAllMode\", null)\n\n}\n\n/*****************************************************************************************************************\n *  Private Helper Functions:\n *****************************************************************************************************************/\n\n/**\n *  encapCommand(cmd)\n *\n *  Applies security or CRC16 encapsulation to a command as needed.\n *  Returns a physicalgraph.zwave.Command.\n **/\nprivate encapCommand(physicalgraph.zwave.Command cmd) {\n    if (state.useSecurity) {\n        return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd)\n    }\n    else if (state.useCrc16) {\n        return zwave.crc16EncapV1.crc16Encap().encapsulate(cmd)\n    }\n    else {\n        return cmd\n    }\n}\n\n/**\n *  prepCommands(cmds, delay=200)\n *\n *  Converts a list of commands (and delays) into a HubMultiAction object, suitable for returning via parse().\n *  Uses encapCommand() to apply security or CRC16 encapsulation as needed.\n **/\nprivate prepCommands(cmds, delay=200) {\n    return response(delayBetween(cmds.collect{ (it instanceof physicalgraph.zwave.Command ) ? encapCommand(it).format() : it },delay))\n}\n\n/**\n *  sendCommands(cmds, delay=200)\n *\n *  Sends a list of commands directly to the device using sendHubCommand.\n *  Uses encapCommand() to apply security or CRC16 encapsulation as needed.\n **/\nprivate sendCommands(cmds, delay=200) {\n    sendHubCommand( cmds.collect{ (it instanceof physicalgraph.zwave.Command ) ? response(encapCommand(it)) : response(it) }, delay)\n}\n\n/**\n *  logger()\n *\n *  Wrapper function for all logging. Simplified for this device handler.\n **/\nprivate logger(msg, level = \"debug\") {\n\n    switch(level) {\n        case \"error\":\n            if (state.loggingLevelIDE >= 1) log.error msg\n            break\n\n        case \"warn\":\n            if (state.loggingLevelIDE >= 2) log.warn msg\n            break\n\n        case \"info\":\n            if (state.loggingLevelIDE >= 3) log.info msg\n            break\n\n        case \"debug\":\n            if (state.loggingLevelIDE >= 4) log.debug msg\n            break\n\n        case \"trace\":\n            if (state.loggingLevelIDE >= 5) log.trace msg\n            break\n\n        default:\n            log.debug msg\n            break\n    }\n}\n\n/**\n *  parseAssocGroupInput(string, maxNodes)\n *\n *  Converts a comma-delimited string of destinations (nodes and endpoints) into an array suitable for passing to\n *  multiChannelAssociationSet(). All numbers are interpreted as hexadecimal. Anything that's not a valid node or\n *  endpoint is discarded (warn). If the list has more than maxNodes, the extras are discarded (warn).\n *\n *  Example input strings:\n *    \"9,A1\"      = Nodes: 9 & 161 (no multi-channel endpoints)            => Output: [9, 161]\n *    \"7,8:1,8:2\" = Nodes: 7, Endpoints: Node8:endpoint1 & node8:endpoint2 => Output: [7, 0, 8, 1, 8, 2]\n */\nprivate parseAssocGroupInput(string, maxNodes) {\n    logger(\"parseAssocGroupInput(): Parsing Association Group Nodes: ${string}\",\"trace\")\n\n    // First split into nodes and endpoints. Count valid entries as we go.\n    if (string) {\n        def nodeList = string.split(',')\n        def nodes = []\n        def endpoints = []\n        def count = 0\n\n        nodeList = nodeList.each { node ->\n            node = node.trim()\n            if ( count >= maxNodes) {\n                logger(\"parseAssocGroupInput(): Number of nodes and endpoints is greater than ${maxNodes}! The following node was discarded: ${node}\",\"warn\")\n            }\n            else if (node.matches(\"\\\\p{XDigit}+\")) { // There's only hexadecimal digits = nodeId\n                def nodeId = Integer.parseInt(node,16)  // Parse as hex\n                if ( (nodeId > 0) & (nodeId < 256) ) { // It's a valid nodeId\n                    nodes << nodeId\n                    count++\n                }\n                else {\n                    logger(\"parseAssocGroupInput(): Invalid nodeId: ${node}\",\"warn\")\n                }\n            }\n            else if (node.matches(\"\\\\p{XDigit}+:\\\\p{XDigit}+\")) { // endpoint e.g. \"0A:2\"\n                def endpoint = node.split(\":\")\n                def nodeId = Integer.parseInt(endpoint[0],16) // Parse as hex\n                def endpointId = Integer.parseInt(endpoint[1],16) // Parse as hex\n                if ( (nodeId > 0) & (nodeId < 256) & (endpointId > 0) & (endpointId < 256) ) { // It's a valid endpoint\n                    endpoints.addAll([nodeId,endpointId])\n                    count++\n                }\n                else {\n                    logger(\"parseAssocGroupInput(): Invalid endpoint: ${node}\",\"warn\")\n                }\n            }\n            else {\n                logger(\"parseAssocGroupInput(): Invalid nodeId: ${node}\",\"warn\")\n            }\n        }\n\n        return (endpoints) ? nodes + [0] + endpoints : nodes\n    }\n    else {\n        return []\n    }\n}\n\n/**\n *  toCcNames()\n *\n *  Convert a list of integers to a list of Z-Wave Command Class Names.\n *\n *  incId  If true, will append the CC Id. E.g. \"CC_NAME (0xAB)\"\n **/\nprivate toCcNames(input, incId = false) {\n\n    def names = getCommandClassNames()\n\n    if (input instanceof Collection) {\n        def out  = []\n        input.each { out.add( names.get(it, 'UNKNOWN') + ((incId) ? \" (${toHexString(it,2,true)})\" : \"\") ) }\n        return out\n    }\n    else {\n        return names.get(input, 'UNKNOWN') + ((incId) ? \" (${toHexString(it,2,true)})\" : \"\")\n    }\n}\n\n/**\n *  toHexString()\n *\n *  Convert a list of integers to a list of hex strings.\n **/\nprivate toHexString(input, size = 2, usePrefix = false) {\n\n    def pattern = (usePrefix) ? \"0x%0${size}X\" : \"%0${size}X\"\n\n    if (input instanceof Collection) {\n        def hex  = []\n        input.each { hex.add(String.format(pattern, it)) }\n        return hex.toString()\n    }\n    else {\n        return String.format(pattern, input)\n    }\n}\n\n/**\n *  sync()\n *\n *  Manages synchronisation of association groups and parameters with the physical device.\n *  The syncPending attribute advertises remaining number of sync operations.\n **/\nprivate sync() {\n    logger(\"sync(): Syncing.\",\"trace\")\n\n    def cmds = []\n    def syncPending = 0\n\n    // Association Group:\n    if (state.zwtAssocGroupTarget != null) { // There's an association group to sync.\n\n        // Check the device supports ASSOCIATION or MULTI_CHANNEL_ASSOCIATION, warn if it doesn't.\n        if (!state.zwtGeneralMd?.commandClassIds.find( {it == 0x85 || it == 0x8E }) ) {\n            logger(\"sync(): Device does not appear to support ASSOCIATION or MULTI_CHANNEL_ASSOCIATION command classes.\",\"warn\")\n        }\n\n        // Check AssocGroupMd for this group:\n        def assocGroupMd = state.zwtAssocGroupsMd.find( { it.id == state.zwtAssocGroupTarget.id } )\n\n        if (!assocGroupMd?.maxNodesSupported) { // Unknown Assocation Group. Request info. Sync will be resumed on receipt of an association report.\n            logger(\"sync(): Unknown association group. Requesting more info.\",\"info\")\n            cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: state.zwtAssocGroupTarget.id) // Try multi-channel first.\n            cmds << zwave.associationV2.associationGet(groupingIdentifier: state.zwtAssocGroupTarget.id)\n            cmds << zwave.associationGrpInfoV1.associationGroupNameGet(groupingIdentifier: state.zwtAssocGroupTarget.id)\n        }\n        else  {\n\n            // Request additional information about the group if it's missing:\n            if (!assocGroupMd.name) {\n                logger(\"sync(): Requesting association group name.\",\"info\")\n                cmds << zwave.associationGrpInfoV1.associationGroupNameGet([groupingIdentifier: state.zwtAssocGroupTarget.id])\n            }\n\n            // Determine whether to use multi-channel\n            def useMultiChannel = false\n            switch (state.zwtAssocGroupTarget.commandClass) {\n                case 0: // Auto-detect:\n                    if (assocGroupMd.multiChannel || state.zwtAssocGroupTarget.nodes.contains(0) ) {\n                        useMultiChannel = true\n                    }\n                break\n\n                case 1: // Force (Single-channel) Association:\n                    useMultiChannel = false\n                    if (state.zwtAssocGroupTarget.nodes.contains(0)) {\n                        logger(\"sync(): Using (Single-channel) Association commands will not work with multi-channel endpoint destinations: ${toHexString(state.zwtAssocGroupTarget.nodes)}\",\"warn\")\n                    }\n                break\n\n                case 2: // Force Multi-channel Association:\n                    useMultiChannel = true\n                break\n            }\n\n            if (useMultiChannel) {\n                logger(\"sync(): Syncing Association Group #${state.zwtAssocGroupTarget.id} using Multi-Channel Association commands. New Destinations: ${toHexString(state.zwtAssocGroupTarget.nodes)}\",\"info\")\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: state.zwtAssocGroupTarget.id)\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: state.zwtAssocGroupTarget.id, nodeId: []) // Remove All\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: state.zwtAssocGroupTarget.id, nodeId: state.zwtAssocGroupTarget.nodes)\n                cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: state.zwtAssocGroupTarget.id)\n            }\n            else { // Use Association:\n                logger(\"sync(): Syncing Association Group #${state.zwtAssocGroupTarget.id} using (Single-channel) Association commands. New Destinations: ${toHexString(state.zwtAssocGroupTarget.nodes)}\",\"info\")\n                cmds << zwave.associationV2.associationGet(groupingIdentifier: state.zwtAssocGroupTarget.id) // Get original value:\n                cmds << zwave.associationV2.associationRemove(groupingIdentifier: state.zwtAssocGroupTarget.id, nodeId: []) // Remove All\n                cmds << zwave.associationV2.associationSet(groupingIdentifier: state.zwtAssocGroupTarget.id, nodeId: state.zwtAssocGroupTarget.nodes)\n                cmds << zwave.associationV2.associationGet(groupingIdentifier: state.zwtAssocGroupTarget.id) // Get new value\n             }\n        }\n\n        syncPending++\n    }\n\n    // Parameter:\n    if (state.zwtParamTarget != null) { // There's a parameter to sync.\n\n        // Check the device supports CONFIGURATION, warn if it doesn't.\n        if (!state.zwtGeneralMd?.commandClassIds.find( {it == 0x70}) ) {\n            logger(\"sync(): Device does not appear to support the CONFIGURATION command class.\",\"warn\")\n        }\n\n        // Check Meta-data for this parameter:\n        def paramMd = state.zwtParamsMd.find( { it.id == state.zwtParamTarget.id } )\n\n        if (!paramMd?.size) { // Unknown Parameter. Request a configurationReport. Sync will be resumed on receipt of a configuration report.\n            logger(\"sync(): Unknown parameter. Requesting more info.\",\"info\")\n            cmds << zwave.configurationV2.configurationGet(parameterNumber: state.zwtParamTarget.id)\n        }\n        else {\n            // Check that target value is within size range.\n            def unsignedMax = (256 ** paramMd.size) -1\n            def signedMax = (256 ** paramMd.size)/2 -1\n            def signedMin = -signedMax -1\n\n            if (state.zwtParamTarget.scaledConfigurationValue > unsignedMax || state.zwtParamTarget.scaledConfigurationValue < signedMin) {\n                logger(\"sync(): Target value for parameter #${state.zwtParamTarget.id} is out of range! \" +\n                \"Parameter Size: ${paramMd.size}, Max Value: ${unsignedMax}, Min Value: ${signedMin}, New Value: ${state.zwtParamTarget.scaledConfigurationValue}\",\"warn\")\n            }\n            else {\n                if (state.zwtParamTarget.scaledConfigurationValue > signedMax) {\n                    def newTarget = state.zwtParamTarget.scaledConfigurationValue - (256 ** paramMd.size)\n                    logger(\"sync(): Target value for parameter #${state.zwtParamTarget.id} is out of range for a signed value. \" +\n                    \"Interpretting value as unsigned and converting from ${state.zwtParamTarget.scaledConfigurationValue} to ${newTarget}\",\"warn\")\n                    state.zwtParamTarget.scaledConfigurationValue = newTarget\n                }\n            logger(\"sync(): Syncing parameter #${state.zwtParamTarget.id}: Size: ${paramMd.size}, New Value: ${state.zwtParamTarget.scaledConfigurationValue}\",\"info\")\n            cmds << zwave.configurationV2.configurationGet(parameterNumber: state.zwtParamTarget.id) // Get current value.\n            cmds << zwave.configurationV2.configurationSet(parameterNumber: state.zwtParamTarget.id, size: paramMd.size, scaledConfigurationValue: state.zwtParamTarget.scaledConfigurationValue)\n            cmds << zwave.configurationV2.configurationGet(parameterNumber: state.zwtParamTarget.id) // Confirm new value.\n            }\n        }\n        syncPending++\n    }\n\n    // Protection:\n    def protectLocalTarget = (settings.zwtProtectLocal != null) ? settings.zwtProtectLocal.toInteger() : (state.zwtGeneralMd?.protectionLocalId ?:0)\n    def protectRFTarget    = (settings.zwtProtectRF != null) ? settings.zwtProtectRF.toInteger() : (state.zwtGeneralMd?.protectionRFId ?:0)\n    if (settings.zwtProtectLocal != null || settings.zwtProtectRF != null) {\n        logger(\"sync(): Syncing Protection Mode: Local: ${protectLocalTarget}, RF: ${protectRFTarget}\",\"info\")\n        // Check the device supports Protection, warn if it doesn't.\n        if (!state.zwtGeneralMd?.commandClassIds.find( {it == 0x75}) ) {\n            logger(\"sync(): Device does not appear to support the PROTECTION command class.\",\"warn\")\n        }\n        // Send the commands, regardless:\n        cmds << zwave.protectionV2.protectionSet(localProtectionState : protectLocalTarget, rfProtectionState: protectRFTarget)\n        cmds << zwave.protectionV2.protectionGet()\n        syncPending++\n    }\n\n    // Switch_All:\n    if (settings.zwtSwitchAllMode  != null) {\n        logger(\"sync(): Syncing Switch_All Mode: ${settings.zwtSwitchAllMode}\",\"info\")\n        // Check the device supports SWITCH_ALL, warn if it doesn't.\n        if (!state.zwtGeneralMd?.commandClassIds.find( {it == 0x27}) ) {\n            logger(\"sync(): Device does not appear to support the SWITCH_ALL command class.\",\"warn\")\n        }\n        // Send the commands, regardless:\n        cmds << zwave.switchAllV1.switchAllSet(mode: settings.zwtSwitchAllMode.toInteger())\n        cmds << zwave.switchAllV1.switchAllGet()\n        syncPending++\n    }\n\n    sendEvent(name: \"syncPending\", value: syncPending, displayed: false)\n    sendCommands(cmds,800)\n}\n\n/**\n *  updateSyncPending()\n *\n *  Updates syncPending attribute, which advertises remaining number of sync operations.\n **/\nprivate updateSyncPending() {\n\n    def syncPending = 0\n\n    if (state.zwtAssocGroupTarget  != null) { // There's an association group target to sync.\n        def cachedNodes = state.zwtAssocGroupsMd.find( { it.id == state.zwtAssocGroupTarget.id } )?.nodes\n        def targetNodes = state.zwtAssocGroupTarget.nodes\n\n        if ( cachedNodes != targetNodes ) {\n            syncPending++\n        }\n    }\n\n    if (state.zwtParamTarget  != null) { // There's a parameter to sync.\n        if ( state.zwtParamsMd?.find( { it.id == state.zwtParamTarget.id } )?.scaledConfigurationValue != state.zwtParamTarget.scaledConfigurationValue) {\n            syncPending++\n        }\n    }\n\n    // Protection:\n    def protectLocalTarget = (settings.zwtProtectLocal != null) ? settings.zwtProtectLocal.toInteger() : state.zwtGeneralMd?.protectionLocalId\n    def protectRFTarget = (settings.zwtProtectRF != null) ? settings.zwtProtectRF.toInteger() : state.zwtGeneralMd?.protectionRFId\n    if (state.zwtGeneralMd?.protectionLocalId != protectLocalTarget || state.zwtGeneralMd?.protectionRFId != protectRFTarget) {\n        syncPending++\n    }\n\n    // Switch_All:\n    if ( (settings.zwtSwitchAllMode != null) & (state.zwtGeneralMd.switchAllModeId != settings.zwtSwitchAllMode?.toInteger()) ) {\n        syncPending++\n    }\n\n    logger(\"updateSyncPending(): syncPending: ${syncPending}\", \"debug\")\n    if ((syncPending == 0) & (device.latestValue(\"syncPending\") > 0)) logger(\"Sync Complete.\", \"info\")\n    sendEvent(name: \"syncPending\", value: syncPending, displayed: false)\n}\n\n/**\n *  cacheCommandMd()\n *\n *  Caches command meta-data.\n *  Translates commandClassId to a name, however commandId is not translated (the lookup would be too much code).\n **/\nprivate cacheCommandMd(cmd, description = \"\", sourceEndpoint = \"\", destinationEndpoint = \"\") {\n\n    // Update commands meta-data cache:\n    if (state.zwtCommandsMd?.find( { it.commandClassId == cmd.commandClassId & it.commandId == cmd.commandId } )) { // Known command type.\n        state.zwtCommandsMd?.collect {\n            if (it.commandClassId == cmd.commandClassId & it.commandId == cmd.commandId) {\n                it.description = description\n                it.parsedCmd = cmd.toString()\n                if (sourceEndpoint) {it.sourceEndpoint = sourceEndpoint}\n                if (destinationEndpoint) {it.destinationEndpoint = destinationEndpoint}\n            }\n        }\n    }\n    else { // New command type:\n        logger(\"zwaveEvent(): New command type discovered.\",\"debug\")\n        def commandMd = [\n            commandClassId: cmd.commandClassId,\n            commandClassName: toCcNames(cmd.commandClassId.toInteger()),\n            commandId: cmd.commandId,\n            description: description,\n            parsedCmd: cmd.toString()\n        ]\n        if (sourceEndpoint) {commandMd.sourceEndpoint = sourceEndpoint}\n        if (destinationEndpoint) {commandMd.destinationEndpoint = destinationEndpoint}\n\n        state.zwtCommandsMd << commandMd\n    }\n\n}\n\n/*****************************************************************************************************************\n *  Static Matadata Functions:\n *****************************************************************************************************************/\n\n/**\n *  getCommandClassVersions()\n *\n *  Returns a map of the command class versions supported by the device. Used by parse() and zwaveEvent() to\n *  extract encapsulated commands from MultiChannelCmdEncap, MultiInstanceCmdEncap, SecurityMessageEncapsulation,\n *  and Crc16Encap messages.\n **/\nprivate getCommandClassVersions() {\n    return [\n        0x20: 1, // Basic V1\n        0x25: 1, // Switch Binary V1\n        0x26: 2, // Switch Multilvel V2\n        0x27: 1, // Switch All V1\n        0x2B: 1, // Scene Activation V1\n        0x30: 2, // Sensor Binary V2\n        0x31: 5, // Sensor Multilevel V5\n        0x32: 3, // Meter V3\n        0x33: 3, // Switch Color V3\n        0x56: 1, // CRC16 Encapsulation V1\n        0x59: 1, // Association Grp Info\n        0x60: 3, // Multi Channel V3\n        0x62: 1, // Door Lock V1\n        0x70: 2, // Configuration V2\n        0x71: 1, // Alarm (Notification) V1\n        0x72: 2, // Manufacturer Specific V2\n        0x73: 1, // Powerlevel V1\n        0x75: 2, // Protection V2\n        0x76: 1, // Lock V1\n        0x84: 1, // Wake Up V1\n        0x85: 2, // Association V2\n        0x86: 1, // Version V1\n        0x8E: 2, // Multi Channel Association V2\n        0x87: 1, // Indicator V1\n        0x98: 1  // Security V1\n   ]\n}\n\n/**\n *  getCommandClassNames()\n *\n *  Returns a map of command class names. Used by toCcNames().\n **/\nprivate getCommandClassNames() {\n    return [\n        0x00: 'NO_OPERATION',\n        0x20: 'BASIC',\n        0x21: 'CONTROLLER_REPLICATION',\n        0x22: 'APPLICATION_STATUS',\n        0x23: 'ZIP',\n        0x24: 'SECURITY_PANEL_MODE',\n        0x25: 'SWITCH_BINARY',\n        0x26: 'SWITCH_MULTILEVEL',\n        0x27: 'SWITCH_ALL',\n        0x28: 'SWITCH_TOGGLE_BINARY',\n        0x29: 'SWITCH_TOGGLE_MULTILEVEL',\n        0x2A: 'CHIMNEY_FAN',\n        0x2B: 'SCENE_ACTIVATION',\n        0x2C: 'SCENE_ACTUATOR_CONF',\n        0x2D: 'SCENE_CONTROLLER_CONF',\n        0x2E: 'SECURITY_PANEL_ZONE',\n        0x2F: 'SECURITY_PANEL_ZONE_SENSOR',\n        0x30: 'SENSOR_BINARY',\n        0x31: 'SENSOR_MULTILEVEL',\n        0x32: 'METER',\n        0x33: 'SWITCH_COLOR',\n        0x34: 'NETWORK_MANAGEMENT_INCLUSION',\n        0x35: 'METER_PULSE',\n        0x36: 'BASIC_TARIFF_INFO',\n        0x37: 'HRV_STATUS',\n        0x38: 'THERMOSTAT_HEATING',\n        0x39: 'HRV_CONTROL',\n        0x3A: 'DCP_CONFIG',\n        0x3B: 'DCP_MONITOR',\n        0x3C: 'METER_TBL_CONFIG',\n        0x3D: 'METER_TBL_MONITOR',\n        0x3E: 'METER_TBL_PUSH',\n        0x3F: 'PREPAYMENT',\n        0x40: 'THERMOSTAT_MODE',\n        0x41: 'PREPAYMENT_ENCAPSULATION',\n        0x42: 'THERMOSTAT_OPERATING_STATE',\n        0x43: 'THERMOSTAT_SETPOINT',\n        0x44: 'THERMOSTAT_FAN_MODE',\n        0x45: 'THERMOSTAT_FAN_STATE',\n        0x46: 'CLIMATE_CONTROL_SCHEDULE',\n        0x47: 'THERMOSTAT_SETBACK',\n        0x48: 'RATE_TBL_CONFIG',\n        0x49: 'RATE_TBL_MONITOR',\n        0x4A: 'TARIFF_CONFIG',\n        0x4B: 'TARIFF_TBL_MONITOR',\n        0x4C: 'DOOR_LOCK_LOGGING',\n        0x4D: 'NETWORK_MANAGEMENT_BASIC',\n        0x4E: 'SCHEDULE_ENTRY_LOCK',\n        0x4F: 'ZIP_6LOWPAN',\n        0x50: 'BASIC_WINDOW_COVERING',\n        0x51: 'MTP_WINDOW_COVERING',\n        0x52: 'NETWORK_MANAGEMENT_PROXY',\n        0x53: 'SCHEDULE',\n        0x54: 'NETWORK_MANAGEMENT_PRIMARY',\n        0x55: 'TRANSPORT_SERVICE',\n        0x56: 'CRC_16_ENCAP',\n        0x57: 'APPLICATION_CAPABILITY',\n        0x58: 'ZIP_ND',\n        0x59: 'ASSOCIATION_GRP_INFO',\n        0x5A: 'DEVICE_RESET_LOCALLY',\n        0x5B: 'CENTRAL_SCENE',\n        0x5C: 'IP_ASSOCIATION',\n        0x5D: 'ANTITHEFT',\n        0x5E: 'ZWAVEPLUS_INFO',\n        0x5F: 'ZIP_GATEWAY',\n        0x60: 'MULTI_CHANNEL',\n        0x61: 'ZIP_PORTAL',\n        0x62: 'DOOR_LOCK',\n        0x63: 'USER_CODE',\n        0x64: 'HUMIDITY_CONTROL_SETPOINT',\n        0x65: 'DMX',\n        0x66: 'BARRIER_OPERATOR',\n        0x67: 'NETWORK_MANAGEMENT_INSTALLATION_MAINTENANCE',\n        0x68: 'ZIP_NAMING',\n        0x69: 'MAILBOX',\n        0x6A: 'WINDOW_COVERING',\n        0x6B: 'IRRIGATION',\n        0x6C: 'SUPERVISION',\n        0x6D: 'HUMIDITY_CONTROL_MODE',\n        0x6E: 'HUMIDITY_CONTROL_OPERATING_STATE',\n        0x6F: 'ENTRY_CONTROL',\n        0x70: 'CONFIGURATION',\n        0x71: 'NOTIFICATION',\n        0x72: 'MANUFACTURER_SPECIFIC',\n        0x73: 'POWERLEVEL',\n        0x74: 'INCLUSION_CONTROLLER',\n        0x75: 'PROTECTION',\n        0x76: 'LOCK',\n        0x77: 'NODE_NAMING',\n        0x7A: 'FIRMWARE_UPDATE_MD',\n        0x7B: 'GROUPING_NAME',\n        0x7C: 'REMOTE_ASSOCIATION_ACTIVATE',\n        0x7D: 'REMOTE_ASSOCIATION',\n        0x80: 'BATTERY',\n        0x81: 'CLOCK',\n        0x82: 'HAIL',\n        0x84: 'WAKE_UP',\n        0x85: 'ASSOCIATION',\n        0x86: 'VERSION',\n        0x87: 'INDICATOR',\n        0x88: 'PROPRIETARY',\n        0x89: 'LANGUAGE',\n        0x8A: 'TIME',\n        0x8B: 'TIME_PARAMETERS',\n        0x8C: 'GEOGRAPHIC_LOCATION',\n        0x8E: 'MULTI_CHANNEL_ASSOCIATION',\n        0x8F: 'MULTI_CMD',\n        0x90: 'ENERGY_PRODUCTION',\n        0x91: 'MANUFACTURER_PROPRIETARY',\n        0x92: 'SCREEN_MD',\n        0x93: 'SCREEN_ATTRIBUTES',\n        0x94: 'SIMPLE_AV_CONTROL',\n        0x95: 'AV_CONTENT_DIRECTORY_MD',\n        0x96: 'AV_RENDERER_STATUS',\n        0x97: 'AV_CONTENT_SEARCH_MD',\n        0x98: 'SECURITY',\n        0x99: 'AV_TAGGING_MD',\n        0x9A: 'IP_CONFIGURATION',\n        0x9B: 'ASSOCIATION_COMMAND_CONFIGURATION',\n        0x9C: 'SENSOR_ALARM',\n        0x9D: 'SILENCE_ALARM',\n        0x9E: 'SENSOR_CONFIGURATION',\n        0x9F: 'SECURITY_2',\n        0xEF: 'MARK',\n        0xF0: 'NON_INTEROPERABLE'\n    ]\n}\n"
  },
  {
    "path": "smartapps/evohome-connect/evohome-connect.groovy",
    "content": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: Evohome (Connect)\n *\n *  Author: David Lomas (codersaur)\n *\n *  Date: 2016-04-05\n *\n *  Version: 0.08\n *\n *  Description:\n *   - Connect your Honeywell Evohome System to SmartThings.\n *   - Requires the Evohome Heating Zone device handler.\n *   - For latest documentation see: https://github.com/codersaur/SmartThings\n *\n *  Version History:\n * \n *   2016-04-05: v0.08\n *    - New 'Update Refresh Time' setting to control polling after making an update.\n *    - poll() - If onlyZoneId is 0, this will force a status update for all zones.\n * \n *   2016-04-04: v0.07\n *    - Additional info log messages.\n * \n *   2016-04-03: v0.06\n *    - Initial Beta Release\n * \n *  To Do:\n *   - Add support for hot water zones (new device handler).\n *   - Tidy up settings: See: http://docs.smartthings.com/en/latest/smartapp-developers-guide/preferences-and-settings.html\n *   - Allow Evohome zones to be (de)selected as part of the setup process.\n *   - Enable notifications if connection to Evohome cloud fails.\n *   - Expose whether thremoStatMode is permanent or temporary, and if temporary for how long. Get from 'tcs.systemModeStatus.*'. i.e thermostatModeUntil\n *   - Investigate if Evohome supports registing a callback so changes in Evohome are pushed to SmartThings instantly (instead of relying on polling).\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *\n */\ndefinition(\n\tname: \"Evohome (Connect)\",\n\tnamespace: \"codersaur\",\n\tauthor: \"David Lomas (codersaur)\",\n\tdescription: \"Connect your Honeywell Evohome System to SmartThings.\",\n\tcategory: \"My Apps\",\n\ticonUrl: \"http://cdn.device-icons.smartthings.com/Home/home1-icn.png\",\n\ticonX2Url: \"http://cdn.device-icons.smartthings.com/Home/home1-icn.png\",\n\ticonX3Url: \"http://cdn.device-icons.smartthings.com/Home/home1-icn.png\",\n\tsingleInstance: true\n)\n\npreferences {\n\n\tsection (\"Evohome:\") {\n\t\tinput \"prefEvohomeUsername\", \"text\", title: \"Username\", required: true, displayDuringSetup: true\n\t\tinput \"prefEvohomePassword\", \"password\", title: \"Password\", required: true, displayDuringSetup: true\n\t\tinput title: \"Advanced Settings:\", displayDuringSetup: true, type: \"paragraph\", element: \"paragraph\", description: \"Change these only if needed\"\n\t\tinput \"prefEvohomeStatusPollInterval\", \"number\", title: \"Polling Interval (minutes)\", range: \"1..60\", defaultValue: 5, required: true, displayDuringSetup: true, description: \"Poll Evohome every n minutes\"\n\t\tinput \"prefEvohomeUpdateRefreshTime\", \"number\", title: \"Update Refresh Time (seconds)\", range: \"2..60\", defaultValue: 3, required: true, displayDuringSetup: true, description: \"Wait n seconds after an update before polling\"\n\t\tinput \"prefEvohomeWindowFuncTemp\", \"decimal\", title: \"Window Function Temperature\", range: \"0..100\", defaultValue: 5.0, required: true, displayDuringSetup: true, description: \"Must match Evohome controller setting\"\n\t\tinput title: \"Thermostat Modes\", description: \"Configure how long thermostat modes are applied for by default. Set to zero to apply modes permanently.\", displayDuringSetup: true, type: \"paragraph\", element: \"paragraph\"\n\t\tinput 'prefThermostatModeDuration', 'number', title: 'Away/Custom/DayOff Mode (days):', range: \"0..99\", defaultValue: 0, required: true, displayDuringSetup: true, description: 'Apply thermostat modes for this many days'\n\t\tinput 'prefThermostatEconomyDuration', 'number', title: 'Economy Mode (hours):', range: \"0..24\", defaultValue: 0, required: true, displayDuringSetup: true, description: 'Apply economy mode for this many hours'\n\t}\n\n\tsection(\"General:\") {\n\t\tinput \"prefDebugMode\", \"bool\", title: \"Enable debug logging?\", defaultValue: true, displayDuringSetup: true\n\t}\n\t\n}\n\n/**********************************************************************\n *  Setup and Configuration Commands:\n **********************************************************************/\n\n/**\n *  installed()\n *\n *  Runs when the app is first installed.\n *\n **/\ndef installed() {\n\n\tatomicState.installedAt = now()\n\tlog.debug \"${app.label}: Installed with settings: ${settings}\"\n\n}\n\n\n/**\n *  uninstalled()\n *\n *  Runs when the app is uninstalled.\n *\n **/\ndef uninstalled() {\n\tif(getChildDevices()) {\n\t\tremoveChildDevices(getChildDevices())\n\t}\n}\n\n\n/**\n *  updated()\n * \n *  Runs when app settings are changed.\n *\n **/\nvoid updated() {\n\n\tif (atomicState.debug) log.debug \"${app.label}: Updating with settings: ${settings}\"\n\n\t// General:\n\tatomicState.debug = settings.prefDebugMode\n\t\n\t// Evohome:\n\tatomicState.evohomeEndpoint = 'https://tccna.honeywell.com'\n\tatomicState.evohomeAuth = [tokenLifetimePercentThreshold : 50] // Auth Token will be refreshed when down to 50% of its lifetime.\n\tatomicState.evohomeStatusPollInterval = settings.prefEvohomeStatusPollInterval // Poll interval for status updates (minutes).\n\tatomicState.evohomeSchedulePollInterval = 60 // Hardcoded to 1hr (minutes).\n\tatomicState.evohomeUpdateRefreshTime = settings.prefEvohomeUpdateRefreshTime // Wait this many seconds after an update before polling.\n\t\n\n\t// Thermostat Mode Durations:\n\tatomicState.thermostatModeDuration = settings.prefThermostatModeDuration\n\tatomicState.thermostatEconomyDuration = settings.prefThermostatEconomyDuration\n\t\n\t// Force Authentication:\n\tauthenticate()\n\n\t// Refresh Subscriptions and Schedules:\n\tmanageSubscriptions()\n\tmanageSchedules()\n\t\n\t// Refresh child device configuration:\n\tgetEvohomeConfig()\n\tupdateChildDeviceConfig()\n\n\t// Run a poll, but defer it so that updated() returns sooner:\n\trunIn(5, \"poll\")\n\n}\n\n\n/**********************************************************************\n *  Management Commands:\n **********************************************************************/\n\n/**\n *  manageSchedules()\n * \n *  Check scheduled tasks have not stalled, and re-schedule if necessary.\n *  Generates a random offset (seconds) for each scheduled task.\n *  \n *  Schedules:\n *   - manageAuth() - every 5 mins.\n *   - poll() - every minute. \n *  \n **/\nvoid manageSchedules() {\n\n\tif (atomicState.debug) log.debug \"${app.label}: manageSchedules()\"\n\n\t// Generate a random offset (1-60):\n\tRandom rand = new Random(now())\n\tdef randomOffset = 0\n\t\n\t// manageAuth (every 5 mins):\n\tif (1==1) { // To Do: Test if schedule has actually stalled.\n\t\tif (atomicState.debug) log.debug \"${app.label}: manageSchedules(): Re-scheduling manageAuth()\"\n\t\ttry {\n\t\t\tunschedule(manageAuth)\n\t\t}\n\t\tcatch(e) {\n\t\t\t//if (atomicState.debug) log.debug \"${app.label}: manageSchedules(): Unschedule failed\"\n\t\t}\n\t\trandomOffset = rand.nextInt(60)\n\t\tschedule(\"${randomOffset} 0/5 * * * ?\", \"manageAuth\")\n\t}\n\n\t// poll():\n\tif (1==1) { // To Do: Test if schedule has actually stalled.\n\t\tif (atomicState.debug) log.debug \"${app.label}: manageSchedules(): Re-scheduling poll()\"\n\t\ttry {\n\t\t\tunschedule(poll)\n\t\t}\n\t\tcatch(e) {\n\t\t\t//if (atomicState.debug) log.debug \"${app.label}: manageSchedules(): Unschedule failed\"\n\t\t}\n\t\trandomOffset = rand.nextInt(60)\n\t\tschedule(\"${randomOffset} 0/1 * * * ?\", \"poll\")\n\t}\n\n}\n\n\n/**\n *  manageSubscriptions()\n * \n *  Unsubscribe/Subscribe.\n **/\nvoid manageSubscriptions() {\n\n\tif (atomicState.debug) log.debug \"${app.label}: manageSubscriptions()\"\n\n\t// Unsubscribe:\n\tunsubscribe()\n\t\n\t// Subscribe to App Touch events:\n\tsubscribe(app,handleAppTouch)\n\t\n}\n\n\n/**\n *  manageAuth()\n * \n *  Ensures authenication token is valid. \n *   Refreshes Auth Token if lifetime has exceeded evohomeAuthTokenLifetimePercentThreshold.\n *   Re-authenticates if Auth Token has expired completely.\n *   Otherwise, done nothing.\n *\n *  Should be scheduled to run every 1-5 minutes.\n **/\nvoid manageAuth() {\n\n\tif (atomicState.debug) log.debug \"${app.label}: manageAuth()\"\n\n\t// Check if Auth Token is valid, if not authenticate:\n\tif (!atomicState.evohomeAuth.authToken) {\n\t\n\t\tlog.info \"${app.label}: manageAuth(): No Auth Token. Authenticating...\"\n\t\tauthenticate()\n\t}\n\telse if (atomicState.evohomeAuthFailed) {\n\t\n\t\tlog.info \"${app.label}: manageAuth(): Auth has failed. Authenticating...\"\n\t\tauthenticate()\n\t}\n\telse if (!atomicState.evohomeAuth.expiresAt.isNumber() || now() >= atomicState.evohomeAuth.expiresAt) {\n\t\n\t\tlog.info \"${app.label}: manageAuth(): Auth Token has expired. Authenticating...\"\n\t\tauthenticate()\n\t}\n\telse {\t\t\n\t\t// Check if Auth Token should be refreshed:\n\t\tdef refreshAt = atomicState.evohomeAuth.expiresAt - ( 1000 * (atomicState.evohomeAuth.tokenLifetime * atomicState.evohomeAuth.tokenLifetimePercentThreshold / 100))\n\t\t\n\t\tif (now() >= refreshAt) {\n\t\t\tlog.info \"${app.label}: manageAuth(): Auth Token needs to be refreshed before it expires.\"\n\t\t\trefreshAuthToken()\n\t\t}\n\t\telse {\n\t\t\tlog.info \"${app.label}: manageAuth(): Auth Token is okay.\"\t\t\n\t\t}\n\t}\n\n}\n\n\n/**\n *  poll(onlyZoneId=-1)\n * \n *  This is the main command that co-ordinates retrieval of information from the Evohome API\n *  and its dissemination to child devices. It should be scheduled to run every minute.\n *\n *  Different types of information are collected on different schedules:\n *   - Zone status information is polled according to ${evohomeStatusPollInterval}.\n *   - Zone schedules are polled according to ${evohomeSchedulePollInterval}.\n *\n *  poll() can be called by a child device when an update has been made, in which case\n *  onlyZoneId will be specified, and only that zone will be updated.\n * \n *  If onlyZoneId is 0, this will force a status update for all zones, igonoring the poll \n *  interval. This should only be used after setThremostatMode() call.\n *\n *  If onlyZoneId is not specified all zones are updated, but only if the relevent poll\n *  interval has been exceeded.\n *\n **/\nvoid poll(onlyZoneId=-1) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: poll(${onlyZoneId})\"\n\t\n\t// Check if there's been an authentication failure:\n\tif (atomicState.evohomeAuthFailed) {\n\t\tmanageAuth()\n\t}\n\t\n\tif (onlyZoneId == 0) { // Force a status update for all zones (used after a thermostatMode update):\n\t\tgetEvohomeStatus()\n\t\tupdateChildDevice()\n\t}\n\telse if (onlyZoneId != -1) { // A zoneId has been specified, so just get the status and update the relevent device:\n\t\tgetEvohomeStatus(onlyZoneId)\n\t\tupdateChildDevice(onlyZoneId)\n\t}\n\telse { // Get status and schedule for all zones, but only if the relevent poll interval has been exceeded: \n\t\n\t\t// Adjust intervals to allow for poll() execution time:\n\t\tdef evohomeStatusPollThresh = (atomicState.evohomeStatusPollInterval * 60) - 30\n\t\tdef evohomeSchedulePollThresh = (atomicState.evohomeSchedulePollInterval * 60) - 30\n\n\t\t// Get zone status:\n\t\tif (!atomicState.evohomeStatusUpdatedAt || atomicState.evohomeStatusUpdatedAt + (1000 * evohomeStatusPollThresh) < now()) {\n\t\t\tgetEvohomeStatus()\n\t\t} \n\n\t\t// Get zone schedules:\n\t\tif (!atomicState.evohomeSchedulesUpdatedAt || atomicState.evohomeSchedulesUpdatedAt + (1000 * evohomeSchedulePollThresh) < now()) {\n\t\t\tgetEvohomeSchedules()\n\t\t}\n\t\t\n\t\t// Update all child devices:\n\t\tupdateChildDevice()\n\t}\n\n}\n\n\n/**********************************************************************\n *  Event Handlers:\n **********************************************************************/\n\n\n/**\n *  handleAppTouch(evt)\n * \n *  App touch event handler.\n *   Used for testing and debugging.\n *\n **/\nvoid handleAppTouch(evt) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: handleAppTouch()\"\n\n\t//manageAuth()\n\t//manageSchedules()\n\t\n\t//getEvohomeConfig()\n\t//updateChildDeviceConfig()\n\t\n\tpoll()\n\n}\n\n\n/**********************************************************************\n *  SmartApp-Child Interface Commands:\n **********************************************************************/\n\n/**\n *  updateChildDeviceConfig()\n * \n *  Add/Remove/Update Child Devices based on atomicState.evohomeConfig\n *  and update their internal state.\n *\n **/\nvoid updateChildDeviceConfig() {\n\n\tif (atomicState.debug) log.debug \"${app.label}: updateChildDeviceConfig()\"\n\t\n\t// Build list of active DNIs, any existing children with DNIs not in here will be deleted.\n\tdef activeDnis = []\n\t\n\t// Iterate through evohomeConfig, adding new Evohome Heating Zone devices where necessary.\n\tatomicState.evohomeConfig.each { loc ->\n\t\tloc.gateways.each { gateway ->\n\t\t\tgateway.temperatureControlSystems.each { tcs ->\n\t\t\t\ttcs.zones.each { zone ->\n\t\t\t\t\t\n\t\t\t\t\tdef dni = generateDni(loc.locationInfo.locationId, gateway.gatewayInfo.gatewayId, tcs.systemId, zone.zoneId )\n\t\t\t\t\tactiveDnis << dni\n\t\t\t\t\t\n\t\t\t\t\tdef values = [\n\t\t\t\t\t\t'debug': atomicState.debug,\n\t\t\t\t\t\t'updateRefreshTime': atomicState.evohomeUpdateRefreshTime,\n\t\t\t\t\t\t'minHeatingSetpoint': formatTemperature(zone?.heatSetpointCapabilities?.minHeatSetpoint),\n\t\t\t\t\t\t'maxHeatingSetpoint': formatTemperature(zone?.heatSetpointCapabilities?.maxHeatSetpoint),\n\t\t\t\t\t\t'temperatureResolution': zone?.heatSetpointCapabilities?.valueResolution,\n\t\t\t\t\t\t'windowFunctionTemperature': formatTemperature(settings.prefEvohomeWindowFuncTemp),\n\t\t\t\t\t\t'zoneType': zone?.zoneType,\n\t\t\t\t\t\t'locationId': loc.locationInfo.locationId,\n\t\t\t\t\t\t'gatewayId': gateway.gatewayInfo.gatewayId,\n\t\t\t\t\t\t'systemId': tcs.systemId,\n\t\t\t\t\t\t'zoneId': zone.zoneId\n\t\t\t\t\t]\n\t\t\t\t\t\n\t\t\t\t\tdef d = getChildDevice(dni)\n\t\t\t\t\tif(!d) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tvalues.put('label', \"${zone.name} Heating Zone (Evohome)\")\n\t\t\t\t\t\t\tlog.info \"${app.label}: updateChildDeviceConfig(): Creating device: Name: ${values.label},  DNI: ${dni}\"\n\t\t                   \td = addChildDevice(app.namespace, \"Evohome Heating Zone\", dni, null, values)\n\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\tlog.error \"${app.label}: updateChildDeviceConfig(): Error creating device: Name: ${values.label}, DNI: ${dni}, Error: ${e}\"\n\t\t\t\t\t\t}\n\t\t\t\t\t} \n\t\t\t\t\t\n\t\t\t\t\tif(d) {\n\t\t\t\t\t\td.generateEvent(values)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\tif (atomicState.debug) log.debug \"${app.label}: updateChildDeviceConfig(): Active DNIs: ${activeDnis}\"\n\t\n\t// Delete Devices:\n\tdef delete = getChildDevices().findAll { !activeDnis.contains(it.deviceNetworkId) }\n\t\n\tif (atomicState.debug) log.debug \"${app.label}: updateChildDeviceConfig(): Found ${delete.size} devices to delete.\"\n\n\tdelete.each {\n\t\tlog.info \"${app.label}: updateChildDeviceConfig(): Deleting device with DNI: ${it.deviceNetworkId}\"\n\t\ttry {\n\t\t\tdeleteChildDevice(it.deviceNetworkId)\n\t\t}\n\t\tcatch(e) {\n\t\t\tlog.error \"${app.label}: updateChildDeviceConfig(): Error deleting device with DNI: ${it.deviceNetworkId}. Error: ${e}\"\n\t\t}\n\t}\n}\n\n\n\n/**\n *  updateChildDevice(onlyZoneId=-1)\n * \n *  Update the attributes of a child device from atomicState.evohomeStatus\n *  and atomicState.evohomeSchedules.\n *  \n *  If onlyZoneId is not specified, then all zones are updated.\n *\n *  Recalculates scheduledSetpoint, nextScheduledSetpoint, and nextScheduledTime.\n *\n **/\nvoid updateChildDevice(onlyZoneId=-1) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: updateChildDevice(${onlyZoneId})\"\n\t\n\tatomicState.evohomeStatus.each { loc ->\n\t\tloc.gateways.each { gateway ->\n\t\t\tgateway.temperatureControlSystems.each { tcs ->\n\t\t\t\ttcs.zones.each { zone ->\n\t\t\t\t\tif (onlyZoneId == -1 || onlyZoneId == zone.zoneId) { // Filter on zoneId if one has been specified.\n\t\t\t\t\t\n\t\t\t\t\t\tdef dni = generateDni(loc.locationId, gateway.gatewayId, tcs.systemId, zone.zoneId)\n\t\t\t\t\t\tdef d = getChildDevice(dni)\n\t\t\t\t\t\tif(d) {\n\t\t\t\t\t\t\tdef schedule = atomicState.evohomeSchedules.find { it.dni == dni}\n\t\t\t\t\t\t\tdef currSw = getCurrentSwitchpoint(schedule.schedule)\n\t\t\t\t\t\t\tdef nextSw = getNextSwitchpoint(schedule.schedule)\n\n\t\t\t\t\t\t\tdef values = [\n\t\t\t\t\t\t\t\t'temperature': formatTemperature(zone?.temperatureStatus?.temperature),\n\t\t\t\t\t\t\t\t//'isTemperatureAvailable': zone?.temperatureStatus?.isAvailable,\n\t\t\t\t\t\t\t\t'heatingSetpoint': formatTemperature(zone?.heatSetpointStatus?.targetTemperature),\n\t\t\t\t\t\t\t\t'thermostatSetpoint': formatTemperature(zone?.heatSetpointStatus?.targetTemperature),\n\t\t\t\t\t\t\t\t'thermostatSetpointMode': formatSetpointMode(zone?.heatSetpointStatus?.setpointMode),\n\t\t\t\t\t\t\t\t'thermostatSetpointUntil': zone?.heatSetpointStatus?.until,\n\t\t\t\t\t\t\t\t'thermostatMode': formatThermostatMode(tcs?.systemModeStatus?.mode),\n\t\t\t\t\t\t\t\t'scheduledSetpoint': formatTemperature(currSw.temperature),\n\t\t\t\t\t\t\t\t'nextScheduledSetpoint': formatTemperature(nextSw.temperature),\n\t\t\t\t\t\t\t\t'nextScheduledTime': nextSw.time\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: updateChildDevice(): Updating Device with DNI: ${dni} with data: ${values}\"\n\t\t\t\t\t\t\td.generateEvent(values)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: updateChildDevice(): Device with DNI: ${dni} does not exist, so skipping status update.\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n\n/**********************************************************************\n *  Evohome API Commands:\n **********************************************************************/\n\n/**\n *  authenticate()\n * \n *  Authenticate to Evohome.\n *\n **/\nprivate authenticate() {\n\n\tif (atomicState.debug) log.debug \"${app.label}: authenticate()\"\n\t\n\tdef requestParams = [\n\t\tmethod: 'POST',\n\t\turi: 'https://tccna.honeywell.com',\n\t\tpath: '/Auth/OAuth/Token',\n\t\theaders: [\n\t\t\t'Authorization': 'Basic YjAxM2FhMjYtOTcyNC00ZGJkLTg4OTctMDQ4YjlhYWRhMjQ5OnRlc3Q=',\n\t\t\t'Accept': 'application/json, application/xml, text/json, text/x-json, text/javascript, text/xml',\n\t\t\t'Content-Type':\t'application/x-www-form-urlencoded; charset=utf-8'\n\t\t],\n\t\tbody: [\n\t\t\t'grant_type':\t'password',\n\t\t\t'scope':\t'EMEA-V1-Basic EMEA-V1-Anonymous EMEA-V1-Get-Current-User-Account',\n\t\t\t'Username':\tsettings.prefEvohomeUsername,\n\t\t\t'Password':\tsettings.prefEvohomePassword\n\t\t]\n\t]\n\n\ttry {\n\t\thttpPost(requestParams) { resp ->\n\t\t\tif(resp.status == 200 && resp.data) {\n\t\t\t\t// Update evohomeAuth:\n\t\t\t\t// We can't just '.put' or '<<' with atomicState, we have to make a temp copy, edit, and then re-assign.\n\t\t\t\tdef tmpAuth = atomicState.evohomeAuth ?: [:]\n\t\t\t    \ttmpAuth.put('lastUpdated' , now())\n\t\t\t\t\ttmpAuth.put('authToken' , resp?.data?.access_token)\n\t\t\t\t\ttmpAuth.put('tokenLifetime' , resp?.data?.expires_in.toInteger() ?: 0)\n\t\t\t\t\ttmpAuth.put('expiresAt' , now() + (tmpAuth.tokenLifetime * 1000))\n\t\t\t\t\ttmpAuth.put('refreshToken' , resp?.data?.refresh_token)\n\t\t\t\tatomicState.evohomeAuth = tmpAuth\n\t\t\t\tatomicState.evohomeAuthFailed = false\n\t\t\t\t\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: authenticate(): New evohomeAuth: ${atomicState.evohomeAuth}\"\n\t\t\t\tdef exp = new Date(tmpAuth.expiresAt)\n\t\t\t\tlog.info \"${app.label}: authenticate(): New Auth Token Expires At: ${exp}\"\n\n\t\t\t\t// Update evohomeHeaders:\n\t\t\t\tdef tmpHeaders = atomicState.evohomeHeaders ?: [:]\n\t\t\t\t\ttmpHeaders.put('Authorization',\"bearer ${atomicState.evohomeAuth.authToken}\")\n\t\t\t\t\ttmpHeaders.put('applicationId', 'b013aa26-9724-4dbd-8897-048b9aada249')\n\t\t\t\t\ttmpHeaders.put('Accept', 'application/json, application/xml, text/json, text/x-json, text/javascript, text/xml')\n\t\t\t\tatomicState.evohomeHeaders = tmpHeaders\n\t\t\t\t\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: authenticate(): New evohomeHeaders: ${atomicState.evohomeHeaders}\"\n\t\t\t\t\n\t\t\t\t// Now get User Account info:\n\t\t\t\tgetEvohomeUserAccount()\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: authenticate(): No Data. Response Status: ${resp.status}\"\n\t\t\t\tatomicState.evohomeAuthFailed = true\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: authenticate(): Error: e.statusCode ${e.statusCode}\"\n\t\tatomicState.evohomeAuthFailed = true\n\t}\n\t\n}\n\n\n/**\n *  refreshAuthToken()\n * \n *  Refresh Auth Token.\n *  If token refresh fails, then authenticate() is called.\n *  Request is simlar to authenticate, but with grant_type = 'refresh_token' and 'refresh_token'.\n *\n **/\nprivate refreshAuthToken() {\n\n\tif (atomicState.debug) log.debug \"${app.label}: refreshAuthToken()\"\n\n\tdef requestParams = [\n\t\tmethod: 'POST',\n\t\turi: 'https://tccna.honeywell.com',\n\t\tpath: '/Auth/OAuth/Token',\n\t\theaders: [\n\t\t\t'Authorization': 'Basic YjAxM2FhMjYtOTcyNC00ZGJkLTg4OTctMDQ4YjlhYWRhMjQ5OnRlc3Q=',\n\t\t\t'Accept': 'application/json, application/xml, text/json, text/x-json, text/javascript, text/xml',\n\t\t\t'Content-Type':\t'application/x-www-form-urlencoded; charset=utf-8'\n\t\t],\n\t\tbody: [\n\t\t\t'grant_type':\t'refresh_token',\n\t\t\t'scope':\t'EMEA-V1-Basic EMEA-V1-Anonymous EMEA-V1-Get-Current-User-Account',\n\t\t\t'refresh_token':\tatomicState.evohomeAuth.refreshToken\n\t\t]\n\t]\n\n\ttry {\n\t\thttpPost(requestParams) { resp ->\n\t\t\tif(resp.status == 200 && resp.data) {\n\t\t\t\t// Update evohomeAuth:\n\t\t\t\t// We can't just '.put' or '<<' with atomicState, we have to make a temp copy, edit, and then re-assign.\n\t\t\t\tdef tmpAuth = atomicState.evohomeAuth ?: [:]\n\t\t\t    \ttmpAuth.put('lastUpdated' , now())\n\t\t\t\t\ttmpAuth.put('authToken' , resp?.data?.access_token)\n\t\t\t\t\ttmpAuth.put('tokenLifetime' , resp?.data?.expires_in.toInteger() ?: 0)\n\t\t\t\t\ttmpAuth.put('expiresAt' , now() + (tmpAuth.tokenLifetime * 1000))\n\t\t\t\t\ttmpAuth.put('refreshToken' , resp?.data?.refresh_token)\n\t\t\t\tatomicState.evohomeAuth = tmpAuth\n\t\t\t\tatomicState.evohomeAuthFailed = false\n\t\t\t\t\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: refreshAuthToken(): New evohomeAuth: ${atomicState.evohomeAuth}\"\n\t\t\t\tdef exp = new Date(tmpAuth.expiresAt)\n\t\t\t\tlog.info \"${app.label}: refreshAuthToken(): New Auth Token Expires At: ${exp}\"\n\n\t\t\t\t// Update evohomeHeaders:\n\t\t\t\tdef tmpHeaders = atomicState.evohomeHeaders ?: [:]\n\t\t\t\t\ttmpHeaders.put('Authorization',\"bearer ${atomicState.evohomeAuth.authToken}\")\n\t\t\t\t\ttmpHeaders.put('applicationId', 'b013aa26-9724-4dbd-8897-048b9aada249')\n\t\t\t\t\ttmpHeaders.put('Accept', 'application/json, application/xml, text/json, text/x-json, text/javascript, text/xml')\n\t\t\t\tatomicState.evohomeHeaders = tmpHeaders\n\t\t\t\t\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: refreshAuthToken(): New evohomeHeaders: ${atomicState.evohomeHeaders}\"\n\t\t\t\t\n\t\t\t\t// Now get User Account info:\n\t\t\t\tgetEvohomeUserAccount()\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: refreshAuthToken(): No Data. Response Status: ${resp.status}\"\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: refreshAuthToken(): Error: e.statusCode ${e.statusCode}\"\n\t\t// If Unauthorized (401) then re-authenticate:\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t\tauthenticate()\n\t\t}\n\t}\n\t\n}\n\n\n/**\n *  getEvohomeUserAccount()\n * \n *  Gets user account info and stores in atomicState.evohomeUserAccount.\n *\n **/\nprivate getEvohomeUserAccount() {\n\n\tlog.info \"${app.label}: getEvohomeUserAccount(): Getting user account information.\"\n\t\n\tdef requestParams = [\n\t\tmethod: 'GET',\n\t\turi: atomicState.evohomeEndpoint,\n\t\tpath: '/WebAPI/emea/api/v1/userAccount',\n\t\theaders: atomicState.evohomeHeaders\n\t]\n\n\ttry {\n\t\thttpGet(requestParams) { resp ->\n\t\t\tif (resp.status == 200 && resp.data) {\n\t\t\t\tatomicState.evohomeUserAccount = resp.data\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeUserAccount(): Data: ${atomicState.evohomeUserAccount}\"\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: getEvohomeUserAccount(): No Data. Response Status: ${resp.status}\"\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: getEvohomeUserAccount(): Error: e.statusCode ${e.statusCode}\"\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t}\n\t}\n}\n\n\n\n/**\n *  getEvohomeConfig()\n * \n *  Gets Evohome configuration for all locations and stores in atomicState.evohomeConfig.\n *\n **/\nprivate getEvohomeConfig() {\n\n\tlog.info \"${app.label}: getEvohomeConfig(): Getting configuration for all locations.\"\n\n\tdef requestParams = [\n\t\tmethod: 'GET',\n\t\turi: atomicState.evohomeEndpoint,\n\t\tpath: '/WebAPI/emea/api/v1/location/installationInfo',\n\t\tquery: [\n\t\t\t'userId': atomicState.evohomeUserAccount.userId,\n\t\t\t'includeTemperatureControlSystems': 'True'\n\t\t],\n\t\theaders: atomicState.evohomeHeaders\n\t]\n\n\ttry {\n\t\thttpGet(requestParams) { resp ->\n\t\t\tif (resp.status == 200 && resp.data) {\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeConfig(): Data: ${resp.data}\"\n\t\t\t\tatomicState.evohomeConfig = resp.data\n\t\t\t\tatomicState.evohomeConfigUpdatedAt = now()\n\t\t\t\treturn null\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: getEvohomeConfig(): No Data. Response Status: ${resp.status}\"\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: getEvohomeConfig(): Error: e.statusCode ${e.statusCode}\"\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t}\n\t\treturn e\n\t}\n}\n\n\n/**\n *  getEvohomeStatus(onlyZoneId=-1)\n * \n *  Gets Evohome Status for specified zone and stores in atomicState.evohomeStatus.\n *  If onlyZoneId is not specified, all zones are updated.\n *\n **/\nprivate getEvohomeStatus(onlyZoneId=-1) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeStatus(${onlyZoneId})\"\n\t\n\tdef newEvohomeStatus = []\n\t\n\tif (onlyZoneId == -1) { // Update all zones (which can be obtained en-masse for each location):\n\t\t\n\t\tlog.info \"${app.label}: getEvohomeStatus(): Getting status for all zones.\"\n\t\t\n\t\tatomicState.evohomeConfig.each { loc ->\n\t\t\tdef locStatus = getEvohomeLocationStatus(loc.locationInfo.locationId)\n\t\t\tif (locStatus) {\n\t\t\t\tnewEvohomeStatus << locStatus\n\t\t\t}\n\t\t}\n\n\t\tif (newEvohomeStatus) {\n\t\t\t// Write out newEvohomeStatus back to atomicState:\n\t\t\tatomicState.evohomeStatus = newEvohomeStatus\n\t\t\tatomicState.evohomeStatusUpdatedAt = now()\n\t\t}\n\t}\n\telse { // Only update the specified zone:\n\t\t\n\t\tlog.info \"${app.label}: getEvohomeStatus(): Getting status for zone ID: ${onlyZoneId}\"\n\t\t\n\t\tdef newZoneStatus = getEvohomeZoneStatus(onlyZoneId)\n\t\tif (newZoneStatus) {\n\t\t\t// Get existing evohomeStatus and update only the specified zone, preserving data for other zones:\n\t\t\t// Have to do this as atomicState.evohomeStatus can only be written in its entirety (as using atomicstate).\n\t\t\t// If mutiple zones are requesting updates at the same time this could cause loss of new data, but\n\t\t\t// the worse case is having out-of-date data for a few minutes...\n\t\t\tnewEvohomeStatus = atomicState.evohomeStatus\n\t\t\tnewEvohomeStatus.each { loc ->\n\t\t\t\tloc.gateways.each { gateway ->\n\t\t\t\t\tgateway.temperatureControlSystems.each { tcs ->\n\t\t\t\t\t\ttcs.zones.each { zone ->\n\t\t\t\t\t\t\tif (onlyZoneId == zone.zoneId) { // This is the zone that must be updated:\n\t\t\t\t\t\t\t\tzone.activeFaults = newZoneStatus.activeFaults\n\t\t\t\t\t\t\t\tzone.heatSetpointStatus = newZoneStatus.heatSetpointStatus\n\t\t\t\t\t\t\t\tzone.temperatureStatus = newZoneStatus.temperatureStatus\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Write out newEvohomeStatus back to atomicState:\n\t\t\tatomicState.evohomeStatus = newEvohomeStatus\n\t\t\t// Note: atomicState.evohomeStatusUpdatedAt is NOT updated.\n\t\t} \n\t}\n}\n\n\n/**\n *  getEvohomeLocationStatus(locationId)\n * \n *  Gets the status for a specific location and returns data as a map.\n *\n *  Called by getEvohomeStatus().\n **/\nprivate getEvohomeLocationStatus(locationId) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeLocationStatus: Location ID: ${locationId}\"\n\t\n\tdef requestParams = [\n\t\t'method': 'GET',\n\t\t'uri': atomicState.evohomeEndpoint,\n\t\t'path': \"/WebAPI/emea/api/v1/location/${locationId}/status\", \n\t\t'query': [ 'includeTemperatureControlSystems': 'True'],\n\t\t'headers': atomicState.evohomeHeaders\n\t]\n\n\ttry {\n\t\thttpGet(requestParams) { resp ->\n\t\t\tif(resp.status == 200 && resp.data) {\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeLocationStatus: Data: ${resp.data}\"\n\t\t\t\treturn resp.data\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: getEvohomeLocationStatus:  No Data. Response Status: ${resp.status}\"\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: getEvohomeLocationStatus: Error: e.statusCode ${e.statusCode}\"\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t}\n\t\treturn false\n\t}\n}\n\n\n/**\n *  getEvohomeZoneStatus(zoneId)\n * \n *  Gets the status for a specific zone and returns data as a map.\n *\n **/\nprivate getEvohomeZoneStatus(zoneId) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeZoneStatus(${zoneId})\"\n\t\n\tdef requestParams = [\n\t\t'method': 'GET',\n\t\t'uri': atomicState.evohomeEndpoint,\n\t\t'path': \"/WebAPI/emea/api/v1/temperatureZone/${zoneId}/status\",\n\t\t'headers': atomicState.evohomeHeaders\n\t]\n\n\ttry {\n\t\thttpGet(requestParams) { resp ->\n\t\t\tif(resp.status == 200 && resp.data) {\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeZoneStatus: Data: ${resp.data}\"\n\t\t\t\treturn resp.data\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: getEvohomeZoneStatus:  No Data. Response Status: ${resp.status}\"\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: getEvohomeZoneStatus: Error: e.statusCode ${e.statusCode}\"\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t}\n\t\treturn false\n\t}\n}\n\n\n/**\n *  getEvohomeSchedules()\n * \n *  Gets Evohome Schedule for each zone and stores in atomicState.evohomeSchedules.\n *\n **/\nprivate getEvohomeSchedules() {\n\n\tlog.info \"${app.label}: getEvohomeSchedules(): Getting schedules for all zones.\"\n\t\t\t\n\tdef evohomeSchedules = []\n\t\t\n\tatomicState.evohomeConfig.each { loc ->\n\t\tloc.gateways.each { gateway ->\n\t\t\tgateway.temperatureControlSystems.each { tcs ->\n\t\t\t\ttcs.zones.each { zone ->\n\t\t\t\t\tdef dni = generateDni(loc.locationInfo.locationId, gateway.gatewayInfo.gatewayId, tcs.systemId, zone.zoneId )\n\t\t\t\t\tdef schedule = getEvohomeZoneSchedule(zone.zoneId)\n\t\t\t\t\tif (schedule) {\n\t\t\t\t\t\tevohomeSchedules << ['zoneId': zone.zoneId, 'dni': dni, 'schedule': schedule]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (evohomeSchedules) {\n\t\t// Write out complete schedules to state:\n\t\tatomicState.evohomeSchedules = evohomeSchedules\n\t\tatomicState.evohomeSchedulesUpdatedAt = now()\n\t}\n\n\treturn evohomeSchedules\n}\n\n\n/**\n *  getEvohomeZoneSchedule(zoneId)\n * \n *  Gets the schedule for a specific zone and returns data as a map.\n *\n **/\nprivate getEvohomeZoneSchedule(zoneId) {\n\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeZoneSchedule(${zoneId})\"\n\t\n\tdef requestParams = [\n\t\t'method': 'GET',\n\t\t'uri': atomicState.evohomeEndpoint,\n\t\t'path': \"/WebAPI/emea/api/v1/temperatureZone/${zoneId}/schedule\",\n\t\t'headers': atomicState.evohomeHeaders\n\t]\n\n\ttry {\n\t\thttpGet(requestParams) { resp ->\n\t\t\tif(resp.status == 200 && resp.data) {\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: getEvohomeZoneSchedule: Data: ${resp.data}\"\n\t\t\t\treturn resp.data\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: getEvohomeZoneSchedule:  No Data. Response Status: ${resp.status}\"\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: getEvohomeZoneSchedule: Error: e.statusCode ${e.statusCode}\"\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t}\n\t\treturn false\n\t}\n}\n\n\n/**\n *  setThermostatMode(systemId, mode, until)\n * \n *  Set thermostat mode for specified controller, until specified time.\n *\n *   systemId:   SystemId of temperatureControlSystem. E.g.: 123456\n *\n *   mode:       String. Either: \"auto\", \"off\", \"economy\", \"away\", \"dayOff\", \"custom\".\n *\n *   until:      (Optional) Time to apply mode until, can be either:\n *                - Date: date object representing when override should end.\n *                - ISO-8601 date string, in format \"yyyy-MM-dd'T'HH:mm:ssXX\", e.g.: \"2016-04-01T00:00:00Z\".\n *                - String: 'permanent'.\n *                - Number: Duration in hours if mode is 'economy', or in days if mode is 'away'/'dayOff'/'custom'.\n *                          Duration will be rounded down to align with Midnight in the local timezone\n *                          (e.g. a duration of 1 day will end at midnight tonight). If 0, mode is permanent.\n *                If 'until' is not specified, a default value is used from the SmartApp settings.\n *\n *   Notes:      'Auto' and 'Off' modes are always permanent.\n *               Thermostat mode is a property of the temperatureControlSystem (i.e. Evohome controller).\n *               Therefore changing the thermostatMode will affect all zones associated with the same controller.\n * \n * \n *  Example usage:\n *   setThermostatMode(123456, 'auto') // Set auto mode permanently, for controller 123456.\n *   setThermostatMode(123456, 'away','2016-04-01T00:00:00Z') // Set away mode until 1st April, for controller 123456.\n *   setThermostatMode(123456, 'dayOff','permanent') // Set dayOff mode permanently, for controller 123456.\n *   setThermostatMode(123456, 'dayOff', 2) // Set dayOff mode for 2 days (ends tomorrow night), for controller 123456.\n *   setThermostatMode(123456, 'economy', 2) // Set economy mode for 2 hours, for controller 123456.\n *\n **/\ndef setThermostatMode(systemId, mode, until=-1) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: setThermostatMode(): SystemID: ${systemId}, Mode: ${mode}, Until: ${until}\"\n\t\n\t// Clean mode (translate to index):\n\tmode = mode.toLowerCase()\n\tint modeIndex\n\tswitch (mode) {\n\t\tcase 'auto':\n\t\t\tmodeIndex = 0\n\t\t\tbreak\n\t\tcase 'off':\n\t\t\tmodeIndex = 1\n\t\t\tbreak\n\t\tcase 'economy':\n\t\t\tmodeIndex = 2\n\t\t\tbreak\n\t\tcase 'away':\n\t\t\tmodeIndex = 3\n\t\t\tbreak\n\t\tcase 'dayoff':\n\t\t\tmodeIndex = 4\n\t\t\tbreak\n\t\tcase 'custom':\n\t\t\tmodeIndex = 6\n\t\t\tbreak\n\t\tdefault:\n\t\t\tlog.error \"${app.label}: setThermostatMode(): Mode: ${mode} is not supported!\"\n\t\t\tmodeIndex = 999\n\t\t\tbreak\n\t}\n\t\n\t// Clean until:\n\tdef untilRes\n\t\n\t// until has not been specified, so determine behaviour from settings:\n\tif (-1 == until && 'economy' == mode) { \n\t\tuntil = atomicState.thermostatEconomyDuration ?: 0 // Use Default duration for economy mode (hours):\n\t}\n\telse if (-1 == until && ( 'away' == mode ||'dayoff' == mode ||'custom' == mode )) {\n\t\tuntil = atomicState.thermostatModeDuration ?: 0 // Use Default duration for other modes (days):\n\t}\n\t\n\t// Convert to date (or 0):    \n\tif ('permanent' == until || 0 == until || -1 == until) {\n\t\tuntilRes = 0\n\t}\n\telse if (until instanceof Date) {\n\t\tuntilRes = until.format(\"yyyy-MM-dd'T'HH:mm:00XX\", TimeZone.getTimeZone('UTC')) // Round to nearest minute.\n\t}\n\telse if (until ==~ /\\d+.*T.*/) { // until is a ISO-8601 date string already, but we'll re-format it anyway to ensure it's in UTC:\n\t\tuntilRes = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", until).format(\"yyyy-MM-dd'T'HH:mm:00XX\", TimeZone.getTimeZone('UTC')) // Round to nearest minute.\n\t}\n\telse if (until.isNumber() && 'economy' == mode) { // until is a duration in hours:\n\t\tuntilRes = new Date( now() + (Math.round(until) * 3600000) ).format(\"yyyy-MM-dd'T'HH:mm:00XX\", TimeZone.getTimeZone('UTC')) // Round to nearest minute.\n\t}\n\telse if (until.isNumber() && ('away' == mode ||'dayoff' == mode ||'custom' == mode )) { // until is a duration in days:\n\t\tuntilRes = new Date( now() + (Math.round(until) * 86400000) ).format(\"yyyy-MM-dd'T'00:00:00XX\", location.timeZone) // Round down to midnight in the LOCAL timezone.\n\t}\n\telse {\n\t\tlog.warn \"${device.label}: setThermostatMode(): until value could not be parsed. Mode will be applied permanently.\"\n\t\tuntilRes = 0\n\t}\n\t\n\t// If mode is away/dayOff/custom the date needs to be rounded down to midnight in the local timezone, then converted back to string again:\n\tif (0 != untilRes && ('away' == mode ||'dayoff' == mode ||'custom' == mode )) { \n\t\tuntilRes = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", untilRes).format(\"yyyy-MM-dd'T'00:00:00XX\", location.timeZone) ).format(\"yyyy-MM-dd'T'HH:mm:00XX\", TimeZone.getTimeZone('UTC'))\n\t}\n\n\t// Build request:\n\tdef body\n\tif (0 == untilRes || 'off' == mode || 'auto' == mode) { // Mode is permanent:\n\t\tbody = ['SystemMode': modeIndex, 'TimeUntil': null, 'Permanent': 'True']\n\t\tlog.info \"${app.label}: setThermostatMode(): System ID: ${systemId}, Mode: ${mode}, Permanent: True\"\n\t}\n\telse { // Mode is temporary:\n\t\tbody = ['SystemMode': modeIndex, 'TimeUntil': untilRes, 'Permanent': 'False']\n\t\tlog.info \"${app.label}: setThermostatMode(): System ID: ${systemId}, Mode: ${mode}, Permanent: False, Until: ${untilRes}\"\n\t}\n\t\n\tdef requestParams = [\n\t\t'uri': atomicState.evohomeEndpoint,\n\t\t'path': \"/WebAPI/emea/api/v1/temperatureControlSystem/${systemId}/mode\", \n\t\t'body': body,\n\t\t'headers': atomicState.evohomeHeaders\n\t]\n\t\n\t// Make request:\n\ttry {\n\t\thttpPutJson(requestParams) { resp ->\n\t\t\tif(resp.status == 201 && resp.data) {\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: setThermostatMode(): Response: ${resp.data}\"\n\t\t\t\treturn null\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: setThermostatMode():  No Data. Response Status: ${resp.status}\"\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: setThermostatMode(): Error: ${e}\"\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t}\n\t\treturn e\n\t}\n}\n\n\n /**\n *  setHeatingSetpoint(zoneId, setpoint, until=-1)\n * \n *  Set heatingSetpoint for specified zoneId, until specified time.\n *\n *   zoneId:     Zone ID of zone, e.g.: \"123456\"\n *\n *   setpoint:   Setpoint temperature, e.g.: \"21.5\". Can be a number or string.\n *\n *   until:      (Optional) Time to apply setpoint until, can be either:\n *                - Date: date object representing when override should end.\n *                - ISO-8601 date string, in format \"yyyy-MM-dd'T'HH:mm:ssXX\", e.g.: \"2016-04-01T00:00:00Z\".\n *                - String: 'permanent'.\n *               If not specified, setpoint will be applied permanently.\n *\n *  Example usage:\n *   setHeatingSetpoint(123456, 21.0) // Set temp of 21.0 permanently, for zone 123456.\n *   setHeatingSetpoint(123456, 21.0, 'permanent') // Set temp of 21.0 permanently, for zone 123456.\n *   setHeatingSetpoint(123456, 21.0, '2016-04-01T00:00:00Z') // Set until specific time, for zone 123456.\n *\n **/\ndef setHeatingSetpoint(zoneId, setpoint, until=-1) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: setHeatingSetpoint(): Zone ID: ${zoneId}, Setpoint: ${setpoint}, Until: ${until}\"\n\t\n\t// Clean setpoint:\n\tsetpoint = formatTemperature(setpoint)\n\t\n\t// Clean until:\n\tdef untilRes\n\tif ('permanent' == until || 0 == until || -1 == until) {\n\t\tuntilRes = 0\n\t}\n\telse if (until instanceof Date) {\n\t\tuntilRes = until.format(\"yyyy-MM-dd'T'HH:mm:00XX\", TimeZone.getTimeZone('UTC')) // Round to nearest minute.\n\t}\n\telse if (until ==~ /\\d+.*T.*/) { // until is a ISO-8601 date string already, but we'll re-format it anyway to ensure it's in UTC:\n\t\tuntilRes = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", until).format(\"yyyy-MM-dd'T'HH:mm:00XX\", TimeZone.getTimeZone('UTC')) // Round to nearest minute.\n\t}\n\telse {\n\t\tlog.warn \"${device.label}: setHeatingSetpoint(): until value could not be parsed. Setpoint will be applied permanently.\"\n\t\tuntilRes = 0\n\t}\n\t\n\t// Build request:\n\tdef body\n\tif (0 == untilRes) { // Permanent:\n\t\tbody = ['HeatSetpointValue': setpoint, 'SetpointMode': 1, 'TimeUntil': null]\n\t\tlog.info \"${app.label}: setHeatingSetpoint(): Zone ID: ${zoneId}, Setpoint: ${setpoint}, Until: Permanent\"\n\t}\n\telse { // Temporary:\n\t\tbody = ['HeatSetpointValue': setpoint, 'SetpointMode': 2, 'TimeUntil': untilRes]\n\t\tlog.info \"${app.label}: setHeatingSetpoint(): Zone ID: ${zoneId}, Setpoint: ${setpoint}, Until: ${untilRes}\"\n\t}\n\t\n\tdef requestParams = [\n\t\t'uri': atomicState.evohomeEndpoint,\n\t\t'path': \"/WebAPI/emea/api/v1/temperatureZone/${zoneId}/heatSetpoint\", \n\t\t'body': body,\n\t\t'headers': atomicState.evohomeHeaders\n\t]\n\t\n\t// Make request:\n\ttry {\n\t\thttpPutJson(requestParams) { resp ->\n\t\t\tif(resp.status == 201 && resp.data) {\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: setHeatingSetpoint(): Response: ${resp.data}\"\n\t\t\t\treturn null\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: setHeatingSetpoint():  No Data. Response Status: ${resp.status}\"\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: setHeatingSetpoint(): Error: ${e}\"\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t}\n\t\treturn e\n\t}\n}\n\n\n/**\n *  clearHeatingSetpoint(zoneId)\n * \n *  Clear the heatingSetpoint for specified zoneId.\n *   zoneId:     Zone ID of zone, e.g.: \"123456\"\n **/\ndef clearHeatingSetpoint(zoneId) {\n\n\tlog.info \"${app.label}: clearHeatingSetpoint(): Zone ID: ${zoneId}\"\n\t\n\t// Build request:\n\tdef requestParams = [\n\t\t'uri': atomicState.evohomeEndpoint,\n\t\t'path': \"/WebAPI/emea/api/v1/temperatureZone/${zoneId}/heatSetpoint\", \n\t\t'body': ['HeatSetpointValue': 0.0, 'SetpointMode': 0, 'TimeUntil': null],\n\t\t'headers': atomicState.evohomeHeaders\n\t]\n\t\n\t// Make request:\n\ttry {\n\t\thttpPutJson(requestParams) { resp ->\n\t\t\tif(resp.status == 201 && resp.data) {\n\t\t\t\tif (atomicState.debug) log.debug \"${app.label}: clearHeatingSetpoint(): Response: ${resp.data}\"\n\t\t\t\treturn null\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlog.error \"${app.label}: clearHeatingSetpoint():  No Data. Response Status: ${resp.status}\"\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t}\n\t} catch (groovyx.net.http.HttpResponseException e) {\n\t\tlog.error \"${app.label}: clearHeatingSetpoint(): Error: ${e}\"\n\t\tif (e.statusCode == 401) {\n\t\t\tatomicState.evohomeAuthFailed = true\n\t\t}\n\t\treturn e\n\t}\n}\n\n\n/**********************************************************************\n *  Helper Commands:\n **********************************************************************/\n \n /**\n *  generateDni(locId,gatewayId,systemId,deviceId)\n * \n *  Generate a device Network ID.\n *  Uses the same format as the official Evohome App, but with a prefix of \"Evohome.\"\n **/\nprivate generateDni(locId,gatewayId,systemId,deviceId) {\n\treturn 'Evohome.' + [ locId, gatewayId, systemId, deviceId ].join('.')\n}\n \n \n/**\n *  formatTemperature(t)\n * \n *  Format temperature value to one decimal place.\n *  t:   can be string, float, bigdecimal...\n *  Returns as string.\n **/\nprivate formatTemperature(t) {\n\treturn Float.parseFloat(\"${t}\").round(1).toString()\n}\n \n \n /**\n *  formatSetpointMode(mode)\n * \n *  Format Evohome setpointMode values to SmartThings values:\n *   \n **/\nprivate formatSetpointMode(mode) {\n \n\tswitch (mode) {\n\t\tcase 'FollowSchedule':\n\t\t\tmode = 'followSchedule'\n\t\t\tbreak\n\t\tcase 'PermanentOverride':\n\t\t\tmode = 'permanentOverride'\n\t\t\tbreak\n\t\tcase 'TemporaryOverride':\n\t\t\tmode = 'temporaryOverride'\n\t\t\tbreak\n\t\tdefault:\n\t\t\tlog.error \"${app.label}: formatSetpointMode(): Mode: ${mode} unknown!\"\n\t\t\tmode = mode.toLowerCase()\n\t\t\tbreak\n\t}\n\n\treturn mode\n}\n \n \n/**\n *  formatThermostatMode(mode)\n * \n *  Translate Evohome thermostatMode values to SmartThings values.\n *   \n **/\nprivate formatThermostatMode(mode) {\n \n\tswitch (mode) {\n\t\tcase 'Auto':\n\t\t\tmode = 'auto'\n\t\t\tbreak\n\t\tcase 'AutoWithEco':\n\t\t\tmode = 'economy'\n\t\t\tbreak\n\t\tcase 'Away':\n\t\t\tmode = 'away'\n\t\t\tbreak\n\t\tcase 'Custom':\n\t\t\tmode = 'custom'\n\t\t\tbreak\n\t\tcase 'DayOff':\n\t\t\tmode = 'dayOff'\n\t\t\tbreak\n\t\tcase 'HeatingOff':\n\t\t\tmode = 'off'\n\t\t\tbreak\n\t\tdefault:\n\t\t\tlog.error \"${app.label}: formatThermostatMode(): Mode: ${mode} unknown!\"\n\t\t\tmode = mode.toLowerCase()\n\t\t\tbreak\n\t}\n\n\treturn mode\n}\n  \n\n/**\n *  getCurrentSwitchpoint(schedule)\n * \n *  Returns the current active switchpoint in the given schedule.\n *  e.g. [timeOfDay:\"23:00:00\", temperature:\"15.0000\"]\n *   \n **/\nprivate getCurrentSwitchpoint(schedule) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: getCurrentSwitchpoint()\"\n\t\n\tCalendar c = new GregorianCalendar()\n\tdef ScheduleToday = schedule.dailySchedules.find { it.dayOfWeek = c.getTime().format(\"EEEE\", location.timeZone) }\n\t\n\t// Sort and find next switchpoint:\n\tScheduleToday.switchpoints.sort {it.timeOfDay}\n\tScheduleToday.switchpoints.reverse(true)\n\tdef currentSwitchPoint = ScheduleToday.switchpoints.find {it.timeOfDay < c.getTime().format(\"HH:mm:ss\", location.timeZone)}\n\t\n\tif (!currentSwitchPoint) {\n\t\t// There are no current switchpoints today, so we must look for the last Switchpoint yesterday.\n\t\tif (atomicState.debug) log.debug \"${app.label}: getCurrentSwitchpoint(): No current switchpoints today, so must look to yesterday's schedule.\"\n\t\tc.add(Calendar.DATE, -1 ) // Subtract one DAY.\n\t\tdef ScheduleYesterday = schedule.dailySchedules.find { it.dayOfWeek = c.getTime().format(\"EEEE\", location.timeZone) }\n\t\tScheduleYesterday.switchpoints.sort {it.timeOfDay}\n\t\tScheduleYesterday.switchpoints.reverse(true)\n\t\tcurrentSwitchPoint = ScheduleYesterday.switchpoints[0] // There will always be one.\n\t}\n\t\n\t// Now construct the switchpoint time as a full ISO-8601 format date string in UTC:\n\tdef localDateStr = c.getTime().format(\"yyyy-MM-dd'T'\", location.timeZone) + currentSwitchPoint.timeOfDay + c.getTime().format(\"XX\", location.timeZone) // Switchpoint in local timezone.\n\tdef isoDateStr = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", localDateStr).format(\"yyyy-MM-dd'T'HH:mm:ssXX\", TimeZone.getTimeZone('UTC')) // Parse and re-format to UTC timezone.    \n\tcurrentSwitchPoint << [ 'time': isoDateStr ]\n\tif (atomicState.debug) log.debug \"${app.label}: getCurrentSwitchpoint(): Current Switchpoint: ${currentSwitchPoint}\"\n\t\n\treturn currentSwitchPoint\n}\n \n\n/**\n *  getNextSwitchpoint(schedule)\n * \n *  Returns the next switchpoint in the given schedule.\n *  e.g. [timeOfDay:\"23:00:00\", temperature:\"15.0000\"]\n *   \n **/\nprivate getNextSwitchpoint(schedule) {\n\n\tif (atomicState.debug) log.debug \"${app.label}: getNextSwitchpoint()\"\n\t\n\tCalendar c = new GregorianCalendar()\n\tdef ScheduleToday = schedule.dailySchedules.find { it.dayOfWeek = c.getTime().format(\"EEEE\", location.timeZone) }\n\t\n\t// Sort and find next switchpoint:\n\tScheduleToday.switchpoints.sort {it.timeOfDay}\n\tdef nextSwitchPoint = ScheduleToday.switchpoints.find {it.timeOfDay > c.getTime().format(\"HH:mm:ss\", location.timeZone)}\n\t\n\tif (!nextSwitchPoint) {\n\t\t// There are no switchpoints left today, so we must look for the first Switchpoint tomorrow.\n\t\tif (atomicState.debug) log.debug \"${app.label}: getNextSwitchpoint(): No more switchpoints today, so must look to tomorrow's schedule.\"\n\t\tc.add(Calendar.DATE, 1 ) // Add one DAY.\n\t\tdef ScheduleTmrw = schedule.dailySchedules.find { it.dayOfWeek = c.getTime().format(\"EEEE\", location.timeZone) }\n\t\tScheduleTmrw.switchpoints.sort {it.timeOfDay}\n\t\tnextSwitchPoint = ScheduleTmrw.switchpoints[0] // There will always be one.\n\t}\n\n\t// Now construct the switchpoint time as a full ISO-8601 format date string in UTC:\n\tdef localDateStr = c.getTime().format(\"yyyy-MM-dd'T'\", location.timeZone) + nextSwitchPoint.timeOfDay + c.getTime().format(\"XX\", location.timeZone) // Switchpoint in local timezone.\n\tdef isoDateStr = new Date().parse(\"yyyy-MM-dd'T'HH:mm:ssXX\", localDateStr).format(\"yyyy-MM-dd'T'HH:mm:ssXX\", TimeZone.getTimeZone('UTC')) // Parse and re-format to UTC timezone.    \n\tnextSwitchPoint << [ 'time': isoDateStr ]\n\tif (atomicState.debug) log.debug \"${app.label}: getNextSwitchpoint(): Next Switchpoint: ${nextSwitchPoint}\"\n\t\n\treturn nextSwitchPoint\n}\n  "
  },
  {
    "path": "smartapps/influxdb-logger/README.md",
    "content": "# InfluxDB Logger\n\nCopyright (c) [David Lomas](https://github.com/codersaur)\n\n## Overview\n\nThis SmartApp logs SmartThings device attributes to an [InfluxDB](https://influxdata.com/) database.\n\n### Key features:\n* Changes to device attributes are immediately logged to InfluxDB.\n* The _Soft-Polling_ feature forces attribute values to be written to the database periodically, even if values haven't changed.\n* Logs Location _Mode_ events.\n* Supports an InfluxDB instance on the local LAN, without needing to route traffic via the cloud.\n* Supports Basic Authentication to InfluxDB database.\n\n## Installation\nFollow [these instructions](https://github.com/codersaur/SmartThings#smartapp-installation-procedure) to install the SmartApp in the SmartThings IDE. However, before publishing the code in the IDE, edit the _getGroupName()_ command (at the bottom of the code) to add the Group IDs for your SmartThings instance. These can be found from the _'My Locations'_ tab in the SmartThings IDE.\n\nFor more information about installing InfluxDB, Grafana, and this SmartApp, [see this guide](http://codersaur.com/2016/04/smartthings-data-visualisation-using-influxdb-and-grafana/).\n\n## Usage\nSmartApp settings:\n\n* **InfluxDB Database**: Specify your InfluxDB instance details in this section.\n* **Polling**: Configure the _Soft-Polling_ interval. All device attribute values will be written to the database at least once per interval. This is useful to ensure attribute values are written to the database, even when they have not changed. Set to zero to disable.\n* **System Monitoring**: Configure which location and hub attributes are logged.\n* **Devices to Monitor**: Specify which device attributes to monitor.\n\n## Version History\n\n#### 2017-04-03: v1.11\n * Supports Basic HTTP Authentication.\n * logger(): Wrapper for all logging.\n * softPoll(): checks that attribute values are != null.\n * postToInfluxDB(): Added callback option to the HubAction object.\n * handleInfluxResponse(): New callback function. Handles response from posts made in postToInfluxDB() and logs errors.\n * updated(): Removed custom attributes for EnergyMeters.\n \n#### 2017-01-30: v1.10\n * Fixed typo in postToInfluxDB().\n\n#### 2016-11-27: v1.09\n * Added support for more capabilities:\n  * Shock Sensors (capability.shockSensor)\n  * Signal Strength Meters (capability.signalStrength)\n  * Sound Sensors (capability.soundSensor)\n  * Tamper Alerts (capability.tamperAlert)\n  * Window Shades (capability.windowShade)\n\n#### 2016-11-27: v1.08\n * Added support for Sound Pressure Level Sensors (capability.soundPressureLevel).\n\n#### 2016-10-30: v1.07\n * Added support for:\n  * Buttons (capability.button)\n  * Carbon Dioxide Detectors (capability.carbonDioxideMeasurement)\n  * Consumables (capability.consumable)\n  * pH Meters (capability.pHMeasurement)\n  * Pressure Sensors (non-standard capability: capability.sensor)\n  * Touch Sensors (capability.touch)\n  * UV Meters (capability.ultravioletIndex)\n  * Voltage Meters (capability.voltageMeasurement)\n * Added support for logging SmartThings Mode changes. [Measurement name: _stMode]\n * Added support for logging SmartThings Location properties (e.g. mode and timeZone) [Measurement name: _stLocation]\n * Added support for logging SmartThings Hub properties (e.g. uptime and firmware version). [Measurement name: _stHub]\n * _handleEvent()_: All device measurements now include groupId, groupName, hubId, hubName, locationId, and locationName as tags.\n * _handleEvent()_: ThreeAxis measurements are split into valueX, valueY, valueZ fields.\n\n#### 2016-09-06: v1.06\n * _escapeStringForInfluxDB()_: Added substitution of apostrophes (uncomment to use).\n\n#### 2016-04-04: v1.05\n * Added subscription to _'scheduledSetpoint'_, _'optimisation'_, and _'windowFunction'_ custom attributes for Evohome thermostats.\n * Added handling of many new string value events.\n * Added a catch-all for any events with string values.\n\n#### 2016-03-22: v1.04\n * Added subscription to _'thermostatSetpointMode'_ custom attribute for Evohome thermostats.\n\n#### 2016-03-10: v1.03\n * Device subscriptions now auto-generated from _state.deviceAttributes_.\n * Soft-polling auto-generated from _state.deviceAttributes_.\n * Better escaping of characters.\n\n#### 2016-03-02: v1.02\n * _softpoll_ automatically sends values to InfluxDB, to give enough points for Grafana to display.\n * switch events now have _value_ and _valueBinary_ fields.\n\n#### 2016-02-29: v1.01\n * Expanded range of device types supported.\n * Uses a generic event handler for all subscriptions.\n * Sends the following tags: device, group, unit.\n * Event.name now maps to the 'measurement' name.\n * Headers and path are stored as state (to avoid recalculating on every event).\n\n#### 2016-02-28: v1.00\n * Initial Version.\n\n## References\n Some useful links relevant to the development of this SmartApp:\n* [SmartThings Capabilities Reference](http://docs.smartthings.com/en/latest/capabilities-reference.html)\n* [InfluxDB Documentation](https://docs.influxdata.com/influxdb/)\n* [Codersaur.com - SmartThings Data Visualisation using InfluxDB and Grafana](http://codersaur.com/2016/04/smartthings-data-visualisation-using-influxdb-and-grafana/)\n\n## License\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n"
  },
  {
    "path": "smartapps/influxdb-logger/influxdb-logger.groovy",
    "content": "/*****************************************************************************************************************\n *  Copyright David Lomas (codersaur)\n *\n *  Name: InfluxDB Logger\n *\n *  Date: 2017-04-03\n *\n *  Version: 1.11\n *\n *  Source: https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger\n *\n *  Author: David Lomas (codersaur)\n *\n *  Description: A SmartApp to log SmartThings device states to an InfluxDB database.\n *\n *  For full information, including installation instructions, exmples, and version history, see:\n *   https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger\n *\n *  IMPORTANT - To enable the resolution of groupNames (i.e. room names), you must manually insert the group IDs\n *   into the getGroupName() command code at the end of this file.\n *\n *  License:\n *   Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except\n *   in compliance with the License. You may obtain a copy of the License at:\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software distributed under the License is distributed\n *   on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License\n *   for the specific language governing permissions and limitations under the License.\n *****************************************************************************************************************/\ndefinition(\n    name: \"InfluxDB Logger\",\n    namespace: \"codersaur\",\n    author: \"David Lomas (codersaur)\",\n    description: \"Log SmartThings device states to InfluxDB\",\n    category: \"My Apps\",\n    iconUrl: \"https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png\",\n    iconX2Url: \"https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png\",\n    iconX3Url: \"https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png\")\n\npreferences {\n\n    section(\"General:\") {\n        //input \"prefDebugMode\", \"bool\", title: \"Enable debug logging?\", defaultValue: true, displayDuringSetup: true\n        input (\n        \tname: \"configLoggingLevelIDE\",\n        \ttitle: \"IDE Live Logging Level:\\nMessages with this level and higher will be logged to the IDE.\",\n        \ttype: \"enum\",\n        \toptions: [\n        \t    \"0\" : \"None\",\n        \t    \"1\" : \"Error\",\n        \t    \"2\" : \"Warning\",\n        \t    \"3\" : \"Info\",\n        \t    \"4\" : \"Debug\",\n        \t    \"5\" : \"Trace\"\n        \t],\n        \tdefaultValue: \"3\",\n            displayDuringSetup: true,\n        \trequired: false\n        )\n    }\n\n    section (\"InfluxDB Database:\") {\n        input \"prefDatabaseHost\", \"text\", title: \"Host\", defaultValue: \"10.10.10.10\", required: true\n        input \"prefDatabasePort\", \"text\", title: \"Port\", defaultValue: \"8086\", required: true\n        input \"prefDatabaseName\", \"text\", title: \"Database Name\", defaultValue: \"\", required: true\n        input \"prefDatabaseUser\", \"text\", title: \"Username\", required: false\n        input \"prefDatabasePass\", \"text\", title: \"Password\", required: false\n    }\n    \n    section(\"Polling:\") {\n        input \"prefSoftPollingInterval\", \"number\", title:\"Soft-Polling interval (minutes)\", defaultValue: 10, required: true\n    }\n    \n    section(\"System Monitoring:\") {\n        input \"prefLogModeEvents\", \"bool\", title:\"Log Mode Events?\", defaultValue: true, required: true\n        input \"prefLogHubProperties\", \"bool\", title:\"Log Hub Properties?\", defaultValue: true, required: true\n        input \"prefLogLocationProperties\", \"bool\", title:\"Log Location Properties?\", defaultValue: true, required: true\n    }\n    \n    section(\"Devices To Monitor:\") {\n        input \"accelerometers\", \"capability.accelerationSensor\", title: \"Accelerometers\", multiple: true, required: false\n        input \"alarms\", \"capability.alarm\", title: \"Alarms\", multiple: true, required: false\n        input \"batteries\", \"capability.battery\", title: \"Batteries\", multiple: true, required: false\n        input \"beacons\", \"capability.beacon\", title: \"Beacons\", multiple: true, required: false\n        input \"buttons\", \"capability.button\", title: \"Buttons\", multiple: true, required: false\n        input \"cos\", \"capability.carbonMonoxideDetector\", title: \"Carbon Monoxide Detectors\", multiple: true, required: false\n        input \"co2s\", \"capability.carbonDioxideMeasurement\", title: \"Carbon Dioxide Detectors\", multiple: true, required: false\n        input \"colors\", \"capability.colorControl\", title: \"Color Controllers\", multiple: true, required: false\n        input \"consumables\", \"capability.consumable\", title: \"Consumables\", multiple: true, required: false\n        input \"contacts\", \"capability.contactSensor\", title: \"Contact Sensors\", multiple: true, required: false\n        input \"doorsControllers\", \"capability.doorControl\", title: \"Door Controllers\", multiple: true, required: false\n        input \"energyMeters\", \"capability.energyMeter\", title: \"Energy Meters\", multiple: true, required: false\n        input \"humidities\", \"capability.relativeHumidityMeasurement\", title: \"Humidity Meters\", multiple: true, required: false\n        input \"illuminances\", \"capability.illuminanceMeasurement\", title: \"Illuminance Meters\", multiple: true, required: false\n        input \"locks\", \"capability.lock\", title: \"Locks\", multiple: true, required: false\n        input \"motions\", \"capability.motionSensor\", title: \"Motion Sensors\", multiple: true, required: false\n        input \"musicPlayers\", \"capability.musicPlayer\", title: \"Music Players\", multiple: true, required: false\n        input \"peds\", \"capability.stepSensor\", title: \"Pedometers\", multiple: true, required: false\n        input \"phMeters\", \"capability.pHMeasurement\", title: \"pH Meters\", multiple: true, required: false\n        input \"powerMeters\", \"capability.powerMeter\", title: \"Power Meters\", multiple: true, required: false\n        input \"presences\", \"capability.presenceSensor\", title: \"Presence Sensors\", multiple: true, required: false\n        input \"pressures\", \"capability.sensor\", title: \"Pressure Sensors\", multiple: true, required: false\n        input \"shockSensors\", \"capability.shockSensor\", title: \"Shock Sensors\", multiple: true, required: false\n        input \"signalStrengthMeters\", \"capability.signalStrength\", title: \"Signal Strength Meters\", multiple: true, required: false\n        input \"sleepSensors\", \"capability.sleepSensor\", title: \"Sleep Sensors\", multiple: true, required: false\n        input \"smokeDetectors\", \"capability.smokeDetector\", title: \"Smoke Detectors\", multiple: true, required: false\n        input \"soundSensors\", \"capability.soundSensor\", title: \"Sound Sensors\", multiple: true, required: false\n\t\tinput \"spls\", \"capability.soundPressureLevel\", title: \"Sound Pressure Level Sensors\", multiple: true, required: false\n\t\tinput \"switches\", \"capability.switch\", title: \"Switches\", multiple: true, required: false\n        input \"switchLevels\", \"capability.switchLevel\", title: \"Switch Levels\", multiple: true, required: false\n        input \"tamperAlerts\", \"capability.tamperAlert\", title: \"Tamper Alerts\", multiple: true, required: false\n        input \"temperatures\", \"capability.temperatureMeasurement\", title: \"Temperature Sensors\", multiple: true, required: false\n        input \"thermostats\", \"capability.thermostat\", title: \"Thermostats\", multiple: true, required: false\n        input \"threeAxis\", \"capability.threeAxis\", title: \"Three-axis (Orientation) Sensors\", multiple: true, required: false\n        input \"touchs\", \"capability.touchSensor\", title: \"Touch Sensors\", multiple: true, required: false\n        input \"uvs\", \"capability.ultravioletIndex\", title: \"UV Sensors\", multiple: true, required: false\n        input \"valves\", \"capability.valve\", title: \"Valves\", multiple: true, required: false\n        input \"volts\", \"capability.voltageMeasurement\", title: \"Voltage Meters\", multiple: true, required: false\n        input \"waterSensors\", \"capability.waterSensor\", title: \"Water Sensors\", multiple: true, required: false\n        input \"windowShades\", \"capability.windowShade\", title: \"Window Shades\", multiple: true, required: false\n    }\n\n}\n\n\n/*****************************************************************************************************************\n *  SmartThings System Commands:\n *****************************************************************************************************************/\n\n/**\n *  installed()\n *\n *  Runs when the app is first installed.\n **/\ndef installed() {\n    state.installedAt = now()\n    state.loggingLevelIDE = 5\n    log.debug \"${app.label}: Installed with settings: ${settings}\" \n}\n\n/**\n *  uninstalled()\n *\n *  Runs when the app is uninstalled.\n **/\ndef uninstalled() {\n    logger(\"uninstalled()\",\"trace\")\n}\n\n/**\n *  updated()\n * \n *  Runs when app settings are changed.\n * \n *  Updates device.state with input values and other hard-coded values.\n *  Builds state.deviceAttributes which describes the attributes that will be monitored for each device collection \n *  (used by manageSubscriptions() and softPoll()).\n *  Refreshes scheduling and subscriptions.\n **/\ndef updated() {\n    logger(\"updated()\",\"trace\")\n\n    // Update internal state:\n    state.loggingLevelIDE = (settings.configLoggingLevelIDE) ? settings.configLoggingLevelIDE.toInteger() : 3\n    \n    // Database config:\n    state.databaseHost = settings.prefDatabaseHost\n    state.databasePort = settings.prefDatabasePort\n    state.databaseName = settings.prefDatabaseName\n    state.databaseUser = settings.prefDatabaseUser\n    state.databasePass = settings.prefDatabasePass \n    \n    state.path = \"/write?db=${state.databaseName}\"\n    state.headers = [:] \n    state.headers.put(\"HOST\", \"${state.databaseHost}:${state.databasePort}\")\n    state.headers.put(\"Content-Type\", \"application/x-www-form-urlencoded\")\n    if (state.databaseUser && state.databasePass) {\n        state.headers.put(\"Authorization\", encodeCredentialsBasic(state.databaseUser, state.databasePass))\n    }\n\n    // Build array of device collections and the attributes we want to report on for that collection:\n    //  Note, the collection names are stored as strings. Adding references to the actual collection \n    //  objects causes major issues (possibly memory issues?).\n    state.deviceAttributes = []\n    state.deviceAttributes << [ devices: 'accelerometers', attributes: ['acceleration']]\n    state.deviceAttributes << [ devices: 'alarms', attributes: ['alarm']]\n    state.deviceAttributes << [ devices: 'batteries', attributes: ['battery']]\n    state.deviceAttributes << [ devices: 'beacons', attributes: ['presence']]\n    state.deviceAttributes << [ devices: 'buttons', attributes: ['button']]\n    state.deviceAttributes << [ devices: 'cos', attributes: ['carbonMonoxide']]\n    state.deviceAttributes << [ devices: 'co2s', attributes: ['carbonDioxide']]\n    state.deviceAttributes << [ devices: 'colors', attributes: ['hue','saturation','color']]\n    state.deviceAttributes << [ devices: 'consumables', attributes: ['consumableStatus']]\n    state.deviceAttributes << [ devices: 'contacts', attributes: ['contact']]\n    state.deviceAttributes << [ devices: 'doorsControllers', attributes: ['door']]\n    state.deviceAttributes << [ devices: 'energyMeters', attributes: ['energy']]\n    state.deviceAttributes << [ devices: 'humidities', attributes: ['humidity']]\n    state.deviceAttributes << [ devices: 'illuminances', attributes: ['illuminance']]\n    state.deviceAttributes << [ devices: 'locks', attributes: ['lock']]\n    state.deviceAttributes << [ devices: 'motions', attributes: ['motion']]\n    state.deviceAttributes << [ devices: 'musicPlayers', attributes: ['status','level','trackDescription','trackData','mute']]\n    state.deviceAttributes << [ devices: 'peds', attributes: ['steps','goal']]\n    state.deviceAttributes << [ devices: 'phMeters', attributes: ['pH']]\n    state.deviceAttributes << [ devices: 'powerMeters', attributes: ['power','voltage','current','powerFactor']]\n    state.deviceAttributes << [ devices: 'presences', attributes: ['presence']]\n    state.deviceAttributes << [ devices: 'pressures', attributes: ['pressure']]\n    state.deviceAttributes << [ devices: 'shockSensors', attributes: ['shock']]\n    state.deviceAttributes << [ devices: 'signalStrengthMeters', attributes: ['lqi','rssi']]\n    state.deviceAttributes << [ devices: 'sleepSensors', attributes: ['sleeping']]\n    state.deviceAttributes << [ devices: 'smokeDetectors', attributes: ['smoke']]\n    state.deviceAttributes << [ devices: 'soundSensors', attributes: ['sound']]\n\tstate.deviceAttributes << [ devices: 'spls', attributes: ['soundPressureLevel']]\n\tstate.deviceAttributes << [ devices: 'switches', attributes: ['switch']]\n    state.deviceAttributes << [ devices: 'switchLevels', attributes: ['level']]\n    state.deviceAttributes << [ devices: 'tamperAlerts', attributes: ['tamper']]\n    state.deviceAttributes << [ devices: 'temperatures', attributes: ['temperature']]\n    state.deviceAttributes << [ devices: 'thermostats', attributes: ['temperature','heatingSetpoint','coolingSetpoint','thermostatSetpoint','thermostatMode','thermostatFanMode','thermostatOperatingState','thermostatSetpointMode','scheduledSetpoint','optimisation','windowFunction']]\n    state.deviceAttributes << [ devices: 'threeAxis', attributes: ['threeAxis']]\n    state.deviceAttributes << [ devices: 'touchs', attributes: ['touch']]\n    state.deviceAttributes << [ devices: 'uvs', attributes: ['ultravioletIndex']]\n    state.deviceAttributes << [ devices: 'valves', attributes: ['contact']]\n    state.deviceAttributes << [ devices: 'volts', attributes: ['voltage']]\n    state.deviceAttributes << [ devices: 'waterSensors', attributes: ['water']]\n    state.deviceAttributes << [ devices: 'windowShades', attributes: ['windowShade']]\n\n    // Configure Scheduling:\n    state.softPollingInterval = settings.prefSoftPollingInterval.toInteger()\n    manageSchedules()\n    \n    // Configure Subscriptions:\n    manageSubscriptions()\n}\n\n/*****************************************************************************************************************\n *  Event Handlers:\n *****************************************************************************************************************/\n\n/**\n *  handleAppTouch(evt)\n * \n *  Used for testing.\n **/\ndef handleAppTouch(evt) {\n    logger(\"handleAppTouch()\",\"trace\")\n    \n    softPoll()\n}\n\n/**\n *  handleModeEvent(evt)\n * \n *  Log Mode changes.\n **/\ndef handleModeEvent(evt) {\n    logger(\"handleModeEvent(): Mode changed to: ${evt.value}\",\"info\")\n\n    def locationId = escapeStringForInfluxDB(location.id)\n    def locationName = escapeStringForInfluxDB(location.name)\n    def mode = '\"' + escapeStringForInfluxDB(evt.value) + '\"'\n\tdef data = \"_stMode,locationId=${locationId},locationName=${locationName} mode=${mode}\"\n    postToInfluxDB(data)\n}\n\n/**\n *  handleEvent(evt)\n *\n *  Builds data to send to InfluxDB.\n *   - Escapes and quotes string values.\n *   - Calculates logical binary values where string values can be \n *     represented as binary values (e.g. contact: closed = 1, open = 0)\n * \n *  Useful references: \n *   - http://docs.smartthings.com/en/latest/capabilities-reference.html\n *   - https://docs.influxdata.com/influxdb/v0.10/guides/writing_data/\n **/\ndef handleEvent(evt) {\n    logger(\"handleEvent(): $evt.displayName($evt.name:$evt.unit) $evt.value\",\"info\")\n    \n    // Build data string to send to InfluxDB:\n    //  Format: <measurement>[,<tag_name>=<tag_value>] field=<field_value>\n    //    If value is an integer, it must have a trailing \"i\"\n    //    If value is a string, it must be enclosed in double quotes.\n    def measurement = evt.name\n    // tags:\n    def deviceId = escapeStringForInfluxDB(evt.deviceId)\n    def deviceName = escapeStringForInfluxDB(evt.displayName)\n    def groupId = escapeStringForInfluxDB(evt?.device.device.groupId)\n    def groupName = escapeStringForInfluxDB(getGroupName(evt?.device.device.groupId))\n    def hubId = escapeStringForInfluxDB(evt?.device.device.hubId)\n    def hubName = escapeStringForInfluxDB(evt?.device.device.hub.toString())\n    // Don't pull these from the evt.device as the app itself will be associated with one location.\n    def locationId = escapeStringForInfluxDB(location.id)\n    def locationName = escapeStringForInfluxDB(location.name)\n\n    def unit = escapeStringForInfluxDB(evt.unit)\n    def value = escapeStringForInfluxDB(evt.value)\n    def valueBinary = ''\n    \n    def data = \"${measurement},deviceId=${deviceId},deviceName=${deviceName},groupId=${groupId},groupName=${groupName},hubId=${hubId},hubName=${hubName},locationId=${locationId},locationName=${locationName}\"\n    \n    // Unit tag and fields depend on the event type:\n    //  Most string-valued attributes can be translated to a binary value too.\n    if ('acceleration' == evt.name) { // acceleration: Calculate a binary value (active = 1, inactive = 0)\n        unit = 'acceleration'\n        value = '\"' + value + '\"'\n        valueBinary = ('active' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('alarm' == evt.name) { // alarm: Calculate a binary value (strobe/siren/both = 1, off = 0)\n        unit = 'alarm'\n        value = '\"' + value + '\"'\n        valueBinary = ('off' == evt.value) ? '0i' : '1i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('button' == evt.name) { // button: Calculate a binary value (held = 1, pushed = 0)\n        unit = 'button'\n        value = '\"' + value + '\"'\n        valueBinary = ('pushed' == evt.value) ? '0i' : '1i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('carbonMonoxide' == evt.name) { // carbonMonoxide: Calculate a binary value (detected = 1, clear/tested = 0)\n        unit = 'carbonMonoxide'\n        value = '\"' + value + '\"'\n        valueBinary = ('detected' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('consumableStatus' == evt.name) { // consumableStatus: Calculate a binary value (\"good\" = 1, \"missing\"/\"replace\"/\"maintenance_required\"/\"order\" = 0)\n        unit = 'consumableStatus'\n        value = '\"' + value + '\"'\n        valueBinary = ('good' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('contact' == evt.name) { // contact: Calculate a binary value (closed = 1, open = 0)\n        unit = 'contact'\n        value = '\"' + value + '\"'\n        valueBinary = ('closed' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('door' == evt.name) { // door: Calculate a binary value (closed = 1, open/opening/closing/unknown = 0)\n        unit = 'door'\n        value = '\"' + value + '\"'\n        valueBinary = ('closed' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('lock' == evt.name) { // door: Calculate a binary value (locked = 1, unlocked = 0)\n        unit = 'lock'\n        value = '\"' + value + '\"'\n        valueBinary = ('locked' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('motion' == evt.name) { // Motion: Calculate a binary value (active = 1, inactive = 0)\n        unit = 'motion'\n        value = '\"' + value + '\"'\n        valueBinary = ('active' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('mute' == evt.name) { // mute: Calculate a binary value (muted = 1, unmuted = 0)\n        unit = 'mute'\n        value = '\"' + value + '\"'\n        valueBinary = ('muted' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('presence' == evt.name) { // presence: Calculate a binary value (present = 1, not present = 0)\n        unit = 'presence'\n        value = '\"' + value + '\"'\n        valueBinary = ('present' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('shock' == evt.name) { // shock: Calculate a binary value (detected = 1, clear = 0)\n        unit = 'shock'\n        value = '\"' + value + '\"'\n        valueBinary = ('detected' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('sleeping' == evt.name) { // sleeping: Calculate a binary value (sleeping = 1, not sleeping = 0)\n        unit = 'sleeping'\n        value = '\"' + value + '\"'\n        valueBinary = ('sleeping' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('smoke' == evt.name) { // smoke: Calculate a binary value (detected = 1, clear/tested = 0)\n        unit = 'smoke'\n        value = '\"' + value + '\"'\n        valueBinary = ('detected' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('sound' == evt.name) { // sound: Calculate a binary value (detected = 1, not detected = 0)\n        unit = 'sound'\n        value = '\"' + value + '\"'\n        valueBinary = ('detected' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('switch' == evt.name) { // switch: Calculate a binary value (on = 1, off = 0)\n        unit = 'switch'\n        value = '\"' + value + '\"'\n        valueBinary = ('on' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('tamper' == evt.name) { // tamper: Calculate a binary value (detected = 1, clear = 0)\n        unit = 'tamper'\n        value = '\"' + value + '\"'\n        valueBinary = ('detected' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('thermostatMode' == evt.name) { // thermostatMode: Calculate a binary value (<any other value> = 1, off = 0)\n        unit = 'thermostatMode'\n        value = '\"' + value + '\"'\n        valueBinary = ('off' == evt.value) ? '0i' : '1i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('thermostatFanMode' == evt.name) { // thermostatFanMode: Calculate a binary value (<any other value> = 1, off = 0)\n        unit = 'thermostatFanMode'\n        value = '\"' + value + '\"'\n        valueBinary = ('off' == evt.value) ? '0i' : '1i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('thermostatOperatingState' == evt.name) { // thermostatOperatingState: Calculate a binary value (heating = 1, <any other value> = 0)\n        unit = 'thermostatOperatingState'\n        value = '\"' + value + '\"'\n        valueBinary = ('heating' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('thermostatSetpointMode' == evt.name) { // thermostatSetpointMode: Calculate a binary value (followSchedule = 0, <any other value> = 1)\n        unit = 'thermostatSetpointMode'\n        value = '\"' + value + '\"'\n        valueBinary = ('followSchedule' == evt.value) ? '0i' : '1i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('threeAxis' == evt.name) { // threeAxis: Format to x,y,z values.\n        unit = 'threeAxis'\n        def valueXYZ = evt.value.split(\",\")\n        def valueX = valueXYZ[0]\n        def valueY = valueXYZ[1]\n        def valueZ = valueXYZ[2]\n        data += \",unit=${unit} valueX=${valueX}i,valueY=${valueY}i,valueZ=${valueZ}i\" // values are integers.\n    }\n    else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, \"\" = 0)\n        unit = 'touch'\n        value = '\"' + value + '\"'\n        valueBinary = ('touched' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('optimisation' == evt.name) { // optimisation: Calculate a binary value (active = 1, inactive = 0)\n        unit = 'optimisation'\n        value = '\"' + value + '\"'\n        valueBinary = ('active' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('windowFunction' == evt.name) { // windowFunction: Calculate a binary value (active = 1, inactive = 0)\n        unit = 'windowFunction'\n        value = '\"' + value + '\"'\n        valueBinary = ('active' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('touch' == evt.name) { // touch: Calculate a binary value (touched = 1, <any other value> = 0)\n        unit = 'touch'\n        value = '\"' + value + '\"'\n        valueBinary = ('touched' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('water' == evt.name) { // water: Calculate a binary value (wet = 1, dry = 0)\n        unit = 'water'\n        value = '\"' + value + '\"'\n        valueBinary = ('wet' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    else if ('windowShade' == evt.name) { // windowShade: Calculate a binary value (closed = 1, <any other value> = 0)\n        unit = 'windowShade'\n        value = '\"' + value + '\"'\n        valueBinary = ('closed' == evt.value) ? '1i' : '0i'\n        data += \",unit=${unit} value=${value},valueBinary=${valueBinary}\"\n    }\n    // Catch any other event with a string value that hasn't been handled:\n    else if (evt.value ==~ /.*[^0-9\\.,-].*/) { // match if any characters are not digits, period, comma, or hyphen.\n\t\tlogger(\"handleEvent(): Found a string value that's not explicitly handled: Device Name: ${deviceName}, Event Name: ${evt.name}, Value: ${evt.value}\",\"warn\")\n        value = '\"' + value + '\"'\n        data += \",unit=${unit} value=${value}\"\n    }\n    // Catch any other general numerical event (carbonDioxide, power, energy, humidity, level, temperature, ultravioletIndex, voltage, etc).\n    else {\n        data += \",unit=${unit} value=${value}\"\n    }\n    \n    // Post data to InfluxDB:\n    postToInfluxDB(data)\n\n}\n\n\n/*****************************************************************************************************************\n *  Main Commands:\n *****************************************************************************************************************/\n\n/**\n *  softPoll()\n *\n *  Executed by schedule.\n * \n *  Forces data to be posted to InfluxDB (even if an event has not been triggered).\n *  Doesn't poll devices, just builds a fake event to pass to handleEvent().\n *\n *  Also calls LogSystemProperties().\n **/\ndef softPoll() {\n    logger(\"softPoll()\",\"trace\")\n    \n    logSystemProperties()\n    \n    // Iterate over each attribute for each device, in each device collection in deviceAttributes:\n    def devs // temp variable to hold device collection.\n    state.deviceAttributes.each { da ->\n        devs = settings.\"${da.devices}\"\n        if (devs && (da.attributes)) {\n            devs.each { d ->\n                da.attributes.each { attr ->\n                    if (d.hasAttribute(attr) && d.latestState(attr)?.value != null) {\n                        logger(\"softPoll(): Softpolling device ${d} for attribute: ${attr}\",\"info\")\n                        // Send fake event to handleEvent():\n                        handleEvent([\n                            name: attr, \n                            value: d.latestState(attr)?.value,\n                            unit: d.latestState(attr)?.unit,\n                            device: d,\n                            deviceId: d.id,\n                            displayName: d.displayName\n                        ])\n                    }\n                }\n            }\n        }\n    }\n\n}\n\n/**\n *  logSystemProperties()\n *\n *  Generates measurements for SmartThings system (hubs and locations) properties.\n **/\ndef logSystemProperties() {\n    logger(\"logSystemProperties()\",\"trace\")\n\n    def locationId = '\"' + escapeStringForInfluxDB(location.id) + '\"'\n    def locationName = '\"' + escapeStringForInfluxDB(location.name) + '\"'\n\n\t// Location Properties:\n    if (prefLogLocationProperties) {\n        try {\n            def tz = '\"' + escapeStringForInfluxDB(location.timeZone.ID) + '\"'\n            def mode = '\"' + escapeStringForInfluxDB(location.mode) + '\"'\n            def hubCount = location.hubs.size()\n            def times = getSunriseAndSunset()\n            def srt = '\"' + times.sunrise.format(\"HH:mm\", location.timeZone) + '\"'\n            def sst = '\"' + times.sunset.format(\"HH:mm\", location.timeZone) + '\"'\n\n            def data = \"_stLocation,locationId=${locationId},locationName=${locationName},latitude=${location.latitude},longitude=${location.longitude},timeZone=${tz} mode=${mode},hubCount=${hubCount}i,sunriseTime=${srt},sunsetTime=${sst}\"\n            postToInfluxDB(data)\n        } catch (e) {\n\t\t    logger(\"logSystemProperties(): Unable to log Location properties: ${e}\",\"error\")\n        }\n\t}\n\n\t// Hub Properties:\n    if (prefLogHubProperties) {\n       \tlocation.hubs.each { h ->\n        \ttry {\n                def hubId = '\"' + escapeStringForInfluxDB(h.id) + '\"'\n                def hubName = '\"' + escapeStringForInfluxDB(h.name) + '\"'\n                def hubIP = '\"' + escapeStringForInfluxDB(h.localIP) + '\"'\n                def hubStatus = '\"' + escapeStringForInfluxDB(h.status) + '\"'\n                def batteryInUse = (\"false\" == h.hub.getDataValue(\"batteryInUse\")) ? \"0i\" : \"1i\"\n                def hubUptime = h.hub.getDataValue(\"uptime\") + 'i'\n                def zigbeePowerLevel = h.hub.getDataValue(\"zigbeePowerLevel\") + 'i'\n                def zwavePowerLevel =  '\"' + escapeStringForInfluxDB(h.hub.getDataValue(\"zwavePowerLevel\")) + '\"'\n                def firmwareVersion =  '\"' + escapeStringForInfluxDB(h.firmwareVersionString) + '\"'\n\n                def data = \"_stHub,locationId=${locationId},locationName=${locationName},hubId=${hubId},hubName=${hubName},hubIP=${hubIP} \"\n                data += \"status=${hubStatus},batteryInUse=${batteryInUse},uptime=${hubUptime},zigbeePowerLevel=${zigbeePowerLevel},zwavePowerLevel=${zwavePowerLevel},firmwareVersion=${firmwareVersion}\"\n                postToInfluxDB(data)\n            } catch (e) {\n\t\t\t\tlogger(\"logSystemProperties(): Unable to log Hub properties: ${e}\",\"error\")\n        \t}\n       \t}\n\n\t}\n\n}\n\n/**\n *  postToInfluxDB()\n *\n *  Posts data to InfluxDB.\n *\n *  Uses hubAction instead of httpPost() in case InfluxDB server is on the same LAN as the Smartthings Hub.\n **/\ndef postToInfluxDB(data) {\n    logger(\"postToInfluxDB(): Posting data to InfluxDB: Host: ${state.databaseHost}, Port: ${state.databasePort}, Database: ${state.databaseName}, Data: [${data}]\",\"debug\")\n    \n    try {\n        def hubAction = new physicalgraph.device.HubAction(\n        \t[\n                method: \"POST\",\n                path: state.path,\n                body: data,\n                headers: state.headers\n            ],\n            null,\n            [ callback: handleInfluxResponse ]\n        )\n\t\t\n        sendHubCommand(hubAction)\n    }\n    catch (Exception e) {\n\t\tlogger(\"postToInfluxDB(): Exception ${e} on ${hubAction}\",\"error\")\n    }\n\n    // For reference, code that could be used for WAN hosts:\n    // def url = \"http://${state.databaseHost}:${state.databasePort}/write?db=${state.databaseName}\" \n    //    try {\n    //      httpPost(url, data) { response ->\n    //          if (response.status != 999 ) {\n    //              log.debug \"Response Status: ${response.status}\"\n    //              log.debug \"Response data: ${response.data}\"\n    //              log.debug \"Response contentType: ${response.contentType}\"\n    //            }\n    //      }\n    //  } catch (e) {\t\n    //      logger(\"postToInfluxDB(): Something went wrong when posting: ${e}\",\"error\")\n    //  }\n}\n\n/**\n *  handleInfluxResponse()\n *\n *  Handles response from post made in postToInfluxDB().\n **/\ndef handleInfluxResponse(physicalgraph.device.HubResponse hubResponse) {\n    if(hubResponse.status >= 400) {\n\t\tlogger(\"postToInfluxDB(): Something went wrong! Response from InfluxDB: Headers: ${hubResponse.headers}, Body: ${hubResponse.body}\",\"error\")\n    }\n}\n\n\n/*****************************************************************************************************************\n *  Private Helper Functions:\n *****************************************************************************************************************/\n\n/**\n *  manageSchedules()\n * \n *  Configures/restarts scheduled tasks: \n *   softPoll() - Run every {state.softPollingInterval} minutes.\n **/\nprivate manageSchedules() {\n\tlogger(\"manageSchedules()\",\"trace\")\n\n    // Generate a random offset (1-60):\n    Random rand = new Random(now())\n    def randomOffset = 0\n    \n    // softPoll:\n    try {\n        unschedule(softPoll)\n    }\n    catch(e) {\n        // logger(\"manageSchedules(): Unschedule failed!\",\"error\")\n    }\n\n    if (state.softPollingInterval > 0) {\n        randomOffset = rand.nextInt(60)\n        logger(\"manageSchedules(): Scheduling softpoll to run every ${state.softPollingInterval} minutes (offset of ${randomOffset} seconds).\",\"trace\")\n        schedule(\"${randomOffset} 0/${state.softPollingInterval} * * * ?\", \"softPoll\")\n    }\n    \n}\n\n/**\n *  manageSubscriptions()\n * \n *  Configures subscriptions.\n **/\nprivate manageSubscriptions() {\n\tlogger(\"manageSubscriptions()\",\"trace\")\n\n    // Unsubscribe:\n    unsubscribe()\n    \n    // Subscribe to App Touch events:\n    subscribe(app,handleAppTouch)\n    \n    // Subscribe to mode events:\n    if (prefLogModeEvents) subscribe(location, \"mode\", handleModeEvent)\n    \n    // Subscribe to device attributes (iterate over each attribute for each device collection in state.deviceAttributes):\n    def devs // dynamic variable holding device collection.\n    state.deviceAttributes.each { da ->\n        devs = settings.\"${da.devices}\"\n        if (devs && (da.attributes)) {\n            da.attributes.each { attr ->\n                logger(\"manageSubscriptions(): Subscribing to attribute: ${attr}, for devices: ${da.devices}\",\"info\")\n                // There is no need to check if all devices in the collection have the attribute.\n                subscribe(devs, attr, handleEvent)\n            }\n        }\n    }\n}\n\n/**\n *  logger()\n *\n *  Wrapper function for all logging.\n **/\nprivate logger(msg, level = \"debug\") {\n\n    switch(level) {\n        case \"error\":\n            if (state.loggingLevelIDE >= 1) log.error msg\n            break\n\n        case \"warn\":\n            if (state.loggingLevelIDE >= 2) log.warn msg\n            break\n\n        case \"info\":\n            if (state.loggingLevelIDE >= 3) log.info msg\n            break\n\n        case \"debug\":\n            if (state.loggingLevelIDE >= 4) log.debug msg\n            break\n\n        case \"trace\":\n            if (state.loggingLevelIDE >= 5) log.trace msg\n            break\n\n        default:\n            log.debug msg\n            break\n    }\n}\n\n/**\n *  encodeCredentialsBasic()\n *\n *  Encode credentials for HTTP Basic authentication.\n **/\nprivate encodeCredentialsBasic(username, password) {\n    return \"Basic \" + \"${username}:${password}\".encodeAsBase64().toString()\n}\n\n/**\n *  escapeStringForInfluxDB()\n *\n *  Escape values to InfluxDB.\n *  \n *  If a tag key, tag value, or field key contains a space, comma, or an equals sign = it must \n *  be escaped using the backslash character \\. Backslash characters do not need to be escaped. \n *  Commas and spaces will also need to be escaped for measurements, though equals signs = do not.\n *\n *  Further info: https://docs.influxdata.com/influxdb/v0.10/write_protocols/write_syntax/\n **/\nprivate escapeStringForInfluxDB(str) {\n    if (str) {\n        str = str.replaceAll(\" \", \"\\\\\\\\ \") // Escape spaces.\n        str = str.replaceAll(\",\", \"\\\\\\\\,\") // Escape commas.\n        str = str.replaceAll(\"=\", \"\\\\\\\\=\") // Escape equal signs.\n        str = str.replaceAll(\"\\\"\", \"\\\\\\\\\\\"\") // Escape double quotes.\n        //str = str.replaceAll(\"'\", \"_\")  // Replace apostrophes with underscores.\n    }\n    else {\n        str = 'null'\n    }\n    return str\n}\n\n/**\n *  getGroupName()\n *\n *  Get the name of a 'Group' (i.e. Room) from its ID.\n *  \n *  This is done manually as there does not appear to be a way to enumerate\n *  groups from a SmartApp currently.\n * \n *  GroupIds can be obtained from the SmartThings IDE under 'My Locations'.\n *\n *  See: https://community.smartthings.com/t/accessing-group-within-a-smartapp/6830\n **/\nprivate getGroupName(id) {\n\n    if (id == null) {return 'Home'}\n    else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Kitchen'}\n    else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Lounge'}\n    else if (id == 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') {return 'Office'}\n    else {return 'Unknown'}    \n}\n"
  }
]