Full Code of codersaur/SmartThings for AI

master ec9ae325344f cached
19 files
802.2 KB
204.2k tokens
1 requests
Download .txt
Showing preview only (826K chars total). Download the full file or copy to clipboard to get everything.
Repository: codersaur/SmartThings
Branch: master
Commit: ec9ae325344f
Files: 19
Total size: 802.2 KB

Directory structure:
gitextract_e8liau_6/

├── LICENSE
├── README.md
├── devices/
│   ├── aeon-home-energy-meter/
│   │   └── aeon-home-energy-meter.groovy
│   ├── evohome/
│   │   └── evohome-heating-zone.groovy
│   ├── fibaro-dimmer-2/
│   │   ├── README.md
│   │   └── fibaro-dimmer-2.groovy
│   ├── fibaro-flood-sensor/
│   │   ├── README.md
│   │   └── fibaro-flood-sensor.groovy
│   ├── fibaro-rgbw-controller/
│   │   ├── README.md
│   │   └── fibaro-rgbw-controller.groovy
│   ├── greenwave-powernode-single/
│   │   ├── README.md
│   │   └── greenwave-powernode-single.groovy
│   ├── philio-dual-relay/
│   │   └── philio-dual-relay.groovy
│   ├── tkb-metering-switch/
│   │   └── tkb-metering-switch.groovy
│   └── zwave-tweaker/
│       ├── README.md
│       └── zwave-tweaker.groovy
└── smartapps/
    ├── evohome-connect/
    │   └── evohome-connect.groovy
    └── influxdb-logger/
        ├── README.md
        └── influxdb-logger.groovy

================================================
FILE CONTENTS
================================================

================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright {yyyy} {name of copyright owner}

   Licensed 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

       http://www.apache.org/licenses/LICENSE-2.0

   Unless 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.


================================================
FILE: README.md
================================================
# SmartThings
https://github.com/codersaur/SmartThings

Copyright (c) 2017 [David Lomas](https://github.com/codersaur)

## Overview

This repository contains device handlers and SmartApps for use with Samsung's [SmartThings](http://www.smartthings.com) home automation platform.

## SmartApps

#### [Evohome (Connect) - BETA](https://github.com/codersaur/SmartThings/tree/master/smartapps/evohome-connect):
 - This SmartApp connects your Honeywell Evohome System to SmartThings.
 - Note, the Evohome Heating Zone device handler (below) must also be installed.

#### [InfluxDB Logger](https://github.com/codersaur/SmartThings/tree/master/smartapps/influxdb-logger):
 - This SmartApp logs SmartThings device attributes to an [InfluxDB](https://influxdata.com/) database.

### SmartApp Installation Procedure

#### Part One: Install the code using the SmartThings IDE

1. Within the SmartThings IDE, click '*My SmartApps*', then '*+ New SmartApp*'. 
2. Select the '*From Code*' tab and paste in the contents of the relevant groovy file.
3. Click '*Create*', and then '*Publish*' *(For Me)*.

#### Part Two: Create a SmartApp instance

1. Using the SmartThings app on your phone, navigate to the '*Marketplace*'.
2. Select '*SmartApps*', then browse to '*My Apps*' at the bottom of the list.
3. Select the new SmartApp, complete the configuration options and press '*Done*'.

**Note:** Some SmartApps may support multiple instances, whereas others may only allow one instance.

## Device Handlers

#### [Aeon Home Energy Meter (GEN2 - UK - 1 Clamp)](https://github.com/codersaur/SmartThings/tree/master/devices/aeon-home-energy-meter):
 - This device handler is written specifically for the Aeon Home Energy Meter Gen2 UK version, with a single clamp.
 - It supports live reporting of energy, power, current, and voltage, as well as energy and cost statistics over multiple pre-defined periods.

#### [Evohome Heating Zone - BETA](https://github.com/codersaur/SmartThings/tree/master/devices/evohome):
 - This device handler is required for the Evohome (Connect) SmartApp.

#### [Fibaro Dimmer 2 (FGD-212)](https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2):
 - 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.
 - 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.  
   <img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-on.png" width="200">

#### [Fibaro Flood Sensor (FGFS-101)](https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-flood-sensor):
 - 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.  
   <img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-flood-sensor/screenshots/ffs-ss-tiles-wet.png" width="200">

#### [Fibaro RGBW Controller (FGRGBWM-441)](https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-rgbw-controller):
 - This device handler is written specifically 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).  
   <img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-rgbw-controller/screenshots/screenshot_rgbw.png" width="200">
 
#### [GreenWave PowerNode (Single) (NS210-G-EN)](https://github.com/codersaur/SmartThings/tree/master/devices/greenwave-powernode-single):
  - 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.  
   <img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/greenwave-powernode-single/screenshots/gwpn-ss-tiles-main.png" width="200">

#### [Philio Dual Relay (PAN04)](https://github.com/codersaur/SmartThings/tree/master/devices/philio-dual-relay):
 - This device handler is written specifically for the Philio Dual Relay (PAN04), when used as a single switch/relay only.
 - It supports live reporting of energy, power, current, voltage, and power factor,  as well as energy and cost statistics over multiple pre-defined periods.
 
#### [TKB Metering Switch (TZ88E-GEN5)](https://github.com/codersaur/SmartThings/tree/master/devices/tkb-metering-switch):
 - This device handler is written specifically for the TKB Metering Switch (TZ88E-GEN5).
 - It supports live reporting of energy, power, current, voltage, and power factor,  as well as energy and cost statistics over multiple pre-defined periods.
 
#### [Z-Wave Tweaker](https://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker):
 - A SmartThings device handler to assist with interrogating and tweaking Z-Wave devices. Useful for end-users and SmartThings developers.  
   <img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/zwave-tweaker/screenshots/zwt-ss-tiles-main.png" width="200">

### Device Handler Installation Procedure

#### Part One: Install the device handler code using the SmartThings IDE

1. Within the SmartThings IDE, click on '*My Device Handlers*'.
2. Click the '*+ Create New Device Handler*' button. 
3. Select the '*From Code*' tab and paste in the contents of the relevant groovy file.
4. Click '*Create*'.
5. Click '*Publish*' *(For Me)*.

#### Part Two: Connect your device to SmartThings

If 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).

During 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.

#### Part Three: Update existing device types

When 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.

1. Within the SmartThings IDE, click on '*My Devices*'.
2. Click on the appropriate device to bring up its properties.
3. Click the '*Edit*' button at the bottom.
4. Change the '*Type*' using the drop-down box (custom devices will be near the bottom of the list).
5. Hit the '*Update*' button at the bottom.

#### Part Four: Update existing device settings

If 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.

1. Within the SmartThings IDE, click on the '*Live Logging*' tab to monitor an messages generated by the following steps. 
2. In the SmartThings app on your phone, navigate to the device (you should find the GUI has changed to reflect the new tiles configuration).
3. Press the gear icon to edit the device's settings.
4. Review each setting to ensure it has a suitable value, then press '*Done*'.
5. Back in the SmartThings IDE, review any messages from the device in the '*Live Logging*' screen. 
 
**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.

## License

Licensed 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:

    http://www.apache.org/licenses/LICENSE-2.0

Unless 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.


================================================
FILE: devices/aeon-home-energy-meter/aeon-home-energy-meter.groovy
================================================
/**
 *  Copyright 2016 David Lomas (codersaur)
 *
 *  Name: Aeon Home Energy Meter (GEN2 - UK - 1 Clamp)
 *
 *  Author: David Lomas (codersaur)
 *
 *  Date: 2017-03-02
 *
 *  Version: 1.03
 *
 *  Description:
 *   - This device handler is written specifically for the Aeon Home Energy Meter Gen2 UK version, with a single clamp.
 *   - Supports live reporting of energy, power, current, and voltage. Press the 'Now' tile to refresh.
 *      (voltage tile is not shown by default, but you can enable it below).
 *   - Supports reporting of energy usage and cost over an ad hoc period, based on the 'energy' figure reported by 
 *     the device. Press the 'Since...' tile to reset.
 *   - Supports additional reporting of energy usage and cost over multiple pre-defined periods:
 *       'Today', 'Last 24 Hours', 'Last 7 Days', 'This Month', 'This Year', and 'Lifetime'
 *     These can be cycled through by pressing the 'statsMode' tile. 
 *   - There's a tile that will reset all Energy Stats periods, but it's hidden by default.
 *   - Key device parameters can be set from the device settings. Refer to the Aeon HEMv2 instruction 
 *     manual for full details.
 *   - If you are re-using this device, please use your own hosting for the icons.
 *
 *  Version History:
 *
 *   2017-03-02: v1.03:
 *    - Fixed tile formatting for Android.
 *    - Limited power attribute to one decimal place.
 *
 *   2016-02-27: v1.02
 *    - Added "Voltage Measurement" capability to metadata (although not currently suppoted by hub).
 *    
 *   2016-02-15: v1.01
 *    - Added reporting of energy usage and cost over multiple pre-defined periods.
 *    - Added ConfigurationReport event parser (useful for debuging).
 *    - Added input preferences for Parameter 2, 4, 8.
 *    - Improved input preference descriptions and ranges.
 *    - Added background colours for mainPower and multi1 tiles. 
 *    - Added Instantaneous £/day figure as a secondary info on multi1.
 *
 *   2016-02-05: v1.0 - Initial Version for HEMv2 UK 1 Clamp.
 *    - Added support for voltage (V) and current (A).
 *    - Added fingerprint for HEMv2.
 *    - Added Refresh and Polling capabilities.
 *    - Added input preferences for reporting intervals.
 *    - Added calculation of total cost, based on CostPerKWh setting.
 * 
 *  To Do:
 *   - Capture out-of-band energy reset.
 *   - Option to specify a '£/day' fixed charge, which is added to all energy cost figures.
 *   - If the use of 'enum' inputs with "multiple: true" is ever fixed by ST, then implement input
 *     preferences to specify Reporting Group Content Flags (Parameters 101-103).
 *
 *  License:
 *   Licensed 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:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless 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.
 *
 */
metadata {
	definition (
		name: "Aeon Home Energy Meter (GEN2 - UK - 1 Clamp)", 
		namespace: "codersaur", 
		author: "David Lomas"
	) 
	{
		capability "Power Meter"
		capability "Energy Meter"
		capability "Voltage Measurement"
		capability "Polling"
		capability "Refresh"
        capability "Configuration"
		capability "Sensor"
        
		command "reset"
        command "resetAllStats"
		command "poll"
        command "refresh"
        command "configure"
        command "updated"
        command "cycleStats"
		command "test"
  
		// Standard (Capability) Attributes:
		attribute "power", "number"
        attribute "energy", "number" // Energy (kWh) as reported by device (ad hoc period).

       // Custom Attributes:
        attribute "current", "number"
        attribute "voltage", "number"
        //attribute "powerFactor", "number" - Not supported.
		attribute "powerCost", "number"  // Instantaneous Cost of Power (£/day)
		attribute "lastReset", "string" // Time that ad hoc reporting was reset.
		attribute "statsMode", "string"
		attribute "costOfEnergy", "number" 
		attribute "energyToday", "number"
		attribute "costOfEnergyToday", "number"
		attribute "energy24Hours", "number"
		attribute "costOfEnergy24Hours", "number"
		attribute "energy7Days", "number"
		attribute "costOfEnergy7Days", "number"
		attribute "energyMonth", "number"
		attribute "costOfEnergyMonth", "number"
		attribute "energyYear", "number"
		attribute "costOfEnergyYear", "number"
		attribute "energyLifetime", "number"
		attribute "costOfEnergyLifetime", "number"

        // Display Attributes:
        // These are only required because the UI lacks number formatting and strips leading zeros.
        attribute "dispPower", "string"
        attribute "dispPowerCost", "string"
        attribute "dispCurrent", "string"
        attribute "dispVoltage", "string"
        //attribute "dispPowerFactor", "string" - Not supported.
        attribute "dispEnergy", "string"
        attribute "dispCostOfEnergy", "string"
        attribute "dispEnergyPeriod", "string"
        attribute "dispCostOfEnergyPeriod", "string"
		
		// Fingerprints:
		fingerprint deviceId: "0x3101", inClusters: "0x70 0x32 0x60 0x85 0x56 0x72 0x86"
	}

	// Tile definitions:
	tiles(scale: 2) {
	
		// Multi Tile:
		multiAttributeTile(name:"multi1", type: "generic", width: 6, height: 4) {
			tileAttribute ("device.power", key: "PRIMARY_CONTROL") {
				attributeState "default", label:'${currentValue} W', backgroundColors: [
					[value: 0, color: "#00cc33"],
					[value: 250, color: "#66cc33"],
					[value: 500, color: "#cccc33"],
					[value: 750, color: "#ffcc33"],
					[value: 1000, color: "#ff9933"], 
					[value: 1500, color: "#ff6633"], 
					[value: 2000, color: "#ff3333"]
				]		
			}
			tileAttribute ("device.dispPowerCost", key: "SECONDARY_CONTROL") {
				attributeState "default", label:'(${currentValue})'
			}
		}
		
		// Instantaneous Values:
		valueTile("instMode", "device.dispPower", decoration: "flat", width: 2, height: 1) {
			state "default", label:'Now:', action:"refresh.refresh", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_refresh.png"
		}
		valueTile("power", "device.dispPower", decoration: "flat", width: 2, height: 1, canChangeIcon: true) {
			state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		}
		valueTile("current", "device.dispCurrent", decoration: "flat", width: 2, height: 1) {
			state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		}
		valueTile("voltage", "device.dispVoltage", decoration: "flat", width: 2, height: 1) {
			state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		}
		//valueTile("powerFactor", "device.dispPowerFactor", decoration: "flat", width: 2, height: 1) {
		//	state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		//}
		
        // Ad Hoc Energy Stats:
		valueTile("lastReset", "device.lastReset", decoration: "flat", width: 2, height: 1) {
			state "default", label:'Since:  ${currentValue}', action:"reset", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_stopwatch_reset.png"
		}
		valueTile("energy", "device.dispEnergy", width: 2, height: 1) {
			state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		}
		valueTile("costOfEnergy", "device.dispCostOfEnergy", width: 2, height: 1) {
			state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		}
		
		// Energy Stats:
        // Needs to be a standardTile to be able to change icon for each state.
		valueTile("statsMode", "device.statsMode", decoration: "flat", canChangeIcon: true, canChangeBackground: true, width: 2, height: 1) {
			state "default", label:'${currentValue}:', action: "cycleStats", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png"
			state "Today", label:'${currentValue}:', action: "cycleStats", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png"
            state "Last 24 Hours", label:'${currentValue}:', action: "cycleStats", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png"
            state "Last 7 Days", label:'${currentValue}:', action: "cycleStats", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png"
            state "This Month", label:'${currentValue}:', action: "cycleStats", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png"
			state "This Year", label:"${currentValue}:", action: "cycleStats", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png"
			state "Lifetime", label:'${currentValue}:', action: "cycleStats", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_cal_cycle.png"
		}
		valueTile("energyPeriod", "device.dispEnergyPeriod", width: 2, height: 1) {
			state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		}
		valueTile("costOfEnergyPeriod", "device.dispCostOfEnergyPeriod", width: 2, height: 1) {
			state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		}
		valueTile("costPerKWH", "device.costPerKWH", decoration: "flat", width: 2, height: 1) {
			state "default", label:'Unit Cost: ${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
		}
		
		// Action Buttons:
		standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
		}
		standardTile("resetAllStats", "device.power", decoration: "flat", width: 2, height: 2) {
			state "default", label:'RESET ALL STATS!', action:"resetAllStats"
		}
		standardTile("configure", "device.power", decoration: "flat", width: 2, height: 2) {
			state "default", label:'', action:"configuration.configure", icon:"st.secondary.configure"
		}
		standardTile("test", "device.power", decoration: "flat", width: 2, height: 2) {
			state "default", label:'Test', action:"test"
		}
		
		// Tile layouts:
		main (["multi1"])
		details([
			// Multi Tile:
			"multi1",
			// Instantaneous Values:
			"instMode","power", "current", //"voltage" ,// "powerFactor",
			// Ad Hoc Stats:
			"lastReset", "energy", "costOfEnergy",
			// Energy Stats:
			"statsMode", "energyPeriod", "costOfEnergyPeriod"//, //"costPerKWH",
			// Action Buttons:
			// "refresh","resetAllStats","configure","test"
		])
	}

	// Preferences:
	preferences {
    	section {
			// Debug Mode:
			input "configDebugMode", "boolean", title: "Enable debug logging?", defaultValue: false, displayDuringSetup: false
			input "configCostPerKWH", "string", title: "Energy Cost (£/kWh)", defaultValue: "0.1316", required: true, displayDuringSetup: false
    	}
		
		section {
			// Native Device Parameters:
			
			input "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
			input "configSelectiveReporting", "boolean", title: "Enable Selective Reporting?", defaultValue: false, required: true, displayDuringSetup: false
			
			// Parameter 4: "Power Change Threshold for Auto-Report - Whole HEM (W)"
			input "configPowerThresholdAbs_HEM", "number", title: "Auto-report Power Threshold (W):", description: "Report power when value changes by... W", defaultValue: 50, range: "0..60000", displayDuringSetup: false
			
			// Parameters 5-7 are not needed for single-clamp version.
			
			// Parameter 8: "Power Percentage Change Threshold for Auto-Report - Whole HEM (%)"
			input "configPowerThresholdPercent_HEM", "number", title: "Auto-report Power Threshold (%):", description: "Report power when value changes by...%", defaultValue: 10, range: "0..100", displayDuringSetup: false
			
			// Parameters 9-11 are not needed for single-clamp version.
			
			// Parameters 101-103 are hard-coded. Will add input preferences if multi-select enum input behaviour is fixed by ST. Currently buggy.
			//  Reporting Group 1 = Power and Current.
			//  Reporting Group 2 = Energy
			//  Reporting Group 3 = Voltage
			
			// Parameter 111: Reporting Group 1 - Report Interval (s):
			input "configReportGroup1Interval", "number", title: "Power/Current Reporting Interval (s):", defaultValue: 60, range: "0..2147483647", displayDuringSetup: false
			
			// Parameter 112: Reporting Group 2 - Report Interval (s):
			input "configReportGroup2Interval", "number", title: "Energy Reporting Interval (s):", defaultValue: 600, range: "0..2147483647", displayDuringSetup: false
			
			// Parameter 113: Reporting Group 3 - Report Interval (s):
			input "configReportGroup3Interval", "number", title: "Voltage Reporting Interval (s):", defaultValue: 600, range: "0..2147483647", displayDuringSetup: false
		}
	}

	
	// simulator metadata
	simulator {
		for (int i = 0; i <= 10000; i += 1000) {
			status "power  ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport(
				scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage()
		}
		for (int i = 0; i <= 100; i += 10) {
			status "energy  ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport(
				scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage()
		}
	}

}

/**********************************************************************
 *  Z-wave Event Handlers.
 **********************************************************************/

/**
 *  parse - Called when messages from a device are received by the hub.
 *
 *  The parse method is responsible for interpreting those messages and returning Event definitions.
 *
 *  String 		description 		- The message from the device.
 **/
def parse(String description) {
	//if (state.debug) log.debug "$device.displayName Parsing raw command: " + description
    
    def result = null
    
	// zwave.parse(): 
    // The second parameter specifies which command version to return for each command type:
    // Aeon Home Energy Meter Gen2 supports:
    //  COMMAND_CLASS_METER_V3 [0x32: 3]
    //  COMMAND_CLASS_CONFIGURATION [0x70: 1]
    //  COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 [0x72: 2]
    //  COMMAND_CLASS_MULTI_CHANNEL V3 [????] - Not needed for single clamp device.
	def cmd = zwave.parse(description, [0x32: 3, 0x70: 1, 0x72: 2])
	if (cmd) {
		if (state.debug) log.debug "$device.displayName zwave.parse() returned: $cmd"
		result = zwaveEvent(cmd)
		if (state.debug) log.debug "$device.displayName zwaveEvent() returned: ${result?.inspect()}"	
	}
	return result
}

/**
 *  COMMAND_CLASS_METER_V3 (0x32)
 *
 *  Integer			deltaTime		    		Time in seconds since last report
 *  Short			meterType		    		Unknown = 0, Electric = 1, Gas = 2, Water = 3
 *  List<Short>		meterValue		    		Meter value as an array of bytes
 *  Double			scaledMeterValue			Meter value as a double
 *  List<Short>		previousMeterValue			Previous meter value as an array of bytes
 *  Double			scaledPreviousMeterValue    Previous meter value as a double
 *  Short			size						The size of the array for the meterValue and previousMeterValue
 *  Short			scale						The scale of the values: "kWh"=0, "kVAh"=1, "Watts"=2, "pulses"=3, "Volts"=4, "Amps"=5, "Power Factor"=6, "Unknown"=7
 *  Short			precision					The decimal precision of the values
 *  Short			rateType					???
 *  Boolean			scale2						???
 **/
def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {
	if (cmd.scale == 0) {
    	// Accumulated Energy (kWh) - Update stats and record energy.
    	state.energy = cmd.scaledMeterValue
		updateStats()
        sendEvent(name: "dispEnergy", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal) + " kWh", displayed: false)
		return createEvent(name: "energy", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal), unit: "kWh")
	}
	else if (cmd.scale == 1) {
    	// Accumulated Energy (kVAh) - Ignore.
		//createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh")
	}
	else if (cmd.scale == 2) {
    	// Instantaneous Power (Watts) - Calculate powerCost and record power:
		state.powerCost = cmd.scaledMeterValue * state.costPerKWH * 0.024
		sendEvent(name: "powerCost", value: state.powerCost, unit: "£/day")
        sendEvent(name: "dispPowerCost", value: "£" + String.format("%.2f",state.powerCost as BigDecimal) + " per day", displayed: false)
        sendEvent(name: "dispPower", value: String.format("%.1f",cmd.scaledMeterValue as BigDecimal) + " W", displayed: false)
        return createEvent(name: "power", value: String.format("%.1f",cmd.scaledMeterValue as BigDecimal), unit: "W")
	}
	else if (cmd.scale == 4) {
    	// Instantaneous Voltage (Volts)
		sendEvent(name: "dispVoltage", value: String.format("%.1f",cmd.scaledMeterValue as BigDecimal) + " V", displayed: false)
        return createEvent(name: "voltage", value: cmd.scaledMeterValue, unit: "V")
	}
	else if (cmd.scale == 5) { 
    	// Instantaneous Current (Amps)
		sendEvent(name: "dispCurrent", value: String.format("%.1f",cmd.scaledMeterValue as BigDecimal) + " A", displayed: false)
        return createEvent(name: "current", value: cmd.scaledMeterValue, unit: "A")
	}
	//else if (cmd.scale == 6) {
    	// Instantaneous Power Factor - Not supported.
	//	sendEvent(name: "dispPowerFactor", value: "PF: " + String.format("%.2f",cmd.scaledMeterValue as BigDecimal), displayed: false)
    //    return createEvent(name: "powerFactor", value: cmd.scaledMeterValue, unit: "PF")
	//}
}


/**
 *  COMMAND_CLASS_CONFIGURATION (0x70)
 *
 **/
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
	// Translate the cmd and log the parameter configuration.

	// Translate value (byte array) back to scaledConfigurationValue (decimal):
    // This should be done in zwave.parse() but isn't implemented yet.
    // See: https://community.smartthings.com/t/zwave-configurationv2-configurationreport-dev-question/9771/6
    // I can't make this work just yet...
	//int value = java.nio.ByteBuffer.wrap(cmd.configurationValue as byte[]).getInt()
    // Instead, a brute force way
    def scValue = 0
    if (cmd.size == 1) { scValue = cmd.configurationValue[0]}
    else if (cmd.size == 2) {  scValue = cmd.configurationValue[1] + (cmd.configurationValue[0] * 0x100) }
    else if (cmd.size == 3) {  scValue = cmd.configurationValue[2] + (cmd.configurationValue[1] * 0x100) + (cmd.configurationValue[0] * 0x10000) }
    else if (cmd.size == 4) {  scValue = cmd.configurationValue[3] + (cmd.configurationValue[2] * 0x100) + (cmd.configurationValue[1] * 0x10000) + (cmd.configurationValue[0] * 0x1000000) }
    
    // Translate parameterNumber to parameterDescription:
    def parameterDescription
    switch (cmd.parameterNumber) {
        case 2:
            parameterDescription = "Energy Detection Mode"
            break
        case 3:
            parameterDescription = "Enable Selective Reporting"
            break
        case 4:
            parameterDescription = "Power Change Threshold for Auto-Report - Whole HEM (W)"
            break
        case 5:
            parameterDescription = "Power Change Threshold for Auto-Report - Clamp 1 (W)"
            break
        case 6:
            parameterDescription = "Power Change Threshold for Auto-Report - Clamp 2 (W)"
            break
        case 7:
            parameterDescription = "Power Change Threshold for Auto-Report - Clamp 3 (W)"
            break
        case 8:
            parameterDescription = "Power Percentage Change Threshold for Auto-Report - Whole HEM (%)"
            break
        case 9:
            parameterDescription = "Power Percentage Change Threshold for Auto-Report - Clamp 1 (%)"
            break
        case 10:
            parameterDescription = "Power Percentage Change Threshold for Auto-Report - Clamp 2 (%)"
            break
        case 11:
            parameterDescription = "Power Percentage Change Threshold for Auto-Report - Clamp 3 (%)"
            break
        case 13:
            parameterDescription = "Enable Reporting CRC16 Encapsulation Command"
            break
        case 101:
            parameterDescription = "Reporting Group 1 - Content Flags"
            break
        case 102:
            parameterDescription = "Reporting Group 2 - Content Flags"
            break
        case 103:
            parameterDescription = "Reporting Group 3 - Content Flags"
            break
        case 111:
            parameterDescription = "Reporting Group 1 - Report Interval (s)"
            break
        case 112:
            parameterDescription = "Reporting Group 2 - Report Interval (s)"
            break
        case 113:
            parameterDescription = "Reporting Group 3 - Report Interval (s)"
            break
        case 200:
            parameterDescription = "Partner ID"
            break
        case 252:
            parameterDescription = "Configuration Locked"
            break
        default:
            parameterDescription = "Unknown Parameter"
	}
    
	//log.debug "$device.displayName: Configuration Report: parameterNumber: $cmd.parameterNumber, parameterDescription: $parameterDescription, size: $cmd.size, scaledConfigurationValue: $scValue"
	createEvent(descriptionText: "$device.displayName: Configuration Report: parameterNumber: $cmd.parameterNumber, parameterDescription: $parameterDescription, size: $cmd.size, scaledConfigurationValue: $scValue", displayed: false)
}

/**
 *  COMMAND_CLASS_MANUFACTURER_SPECIFIC_V2 (0x72)
 *
 *  
 **/
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
	def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
	if (state.debug) log.debug "$device.displayName: MSR: $msr"
	updateDataValue("MSR", msr)

	// Apply Manufacturer- or Product-specific configuration here...
}

/**
 *  Default event handler.
 *
 *  Called for all events that aren't handled above.
 **/
def zwaveEvent(physicalgraph.zwave.Command cmd) {
	if (state.debug) log.warn "$device.displayName: Unhandled: $cmd"
	[:]
}


/**********************************************************************
 *  Capability-related Commands:
 **********************************************************************/


/**
 *  refresh() - Refreshes values from the device.
 *
 *  Required for the "Refresh" capability.
 **/
def refresh() {
	delayBetween([
		zwave.meterV3.meterGet(scale: 0).format(), // Energy
		zwave.meterV3.meterGet(scale: 2).format(), // Power
		zwave.meterV3.meterGet(scale: 4).format(), // Volts
		//zwave.meterV3.meterGet(scale: 5).format(), // Current - Not included, as a request will be triggered when Power report is received.
		//zwave.meterV3.meterGet(scale: 6).format() // Power Factor - Not Supported.
	])
}


/**
 *  poll() - Polls the device.
 *
 *  Required for the "Polling" capability
 **/
def poll() {
	refresh()
}


/**
 *  reset() - Reset the Accumulated Energy figure held in the device.
 *
 *  Custom energy reporting period stats are preserved.
 **/
def reset() {
	if (state.debug) log.debug "Reseting Accumulated Energy"
    state.lastReset = new Date().format("YYYY/MM/dd \n HH:mm:ss", location.timeZone)
    sendEvent(name: "lastReset", value: state.lastReset, unit: "")
	
	// Record energy<Period> in energy<Period>Prev:
	state.energyTodayPrev = state.energyToday
	state.energyTodayStart = 0.00
	state.energyMonthPrev = state.energyMonth
	state.energyMonthStart = 0.00
	state.energyYearPrev = state.energyYear
	state.energyYearStart = 0.00
    state.energyLifetimePrev = state.energyLifetime
	state.energy = 0.00
    
    return [
		zwave.meterV3.meterReset().format(),
		zwave.meterV3.meterGet(scale: 0).format()
	]
}


/**********************************************************************
 *  Other Commands:
 **********************************************************************/


/**
 *  resetAllStats() - Reset all Accumulated Energy statistics (!)
 *
 *  Resets the Accumulated Energy figure held in the device AND resets all custom energy reporting period stats!
 **/
def resetAllStats() {
	if (state.debug) log.debug "Reseting All Accumulated Energy Stats!"
    state.lastReset = new Date().format("YYYY/MM/dd \n HH:mm:ss", location.timeZone)
    sendEvent(name: "lastReset", value: state.lastReset, unit: "")
	
	// Reset all energy<Period>Prev/Start values:
	state.energyTodayPrev = 0.00
	state.energyTodayStart = 0.00
	state.energyMonthPrev = 0.00
	state.energyMonthStart = 0.00
	state.energyYearPrev = 0.00
	state.energyYearStart = 0.00
    state.energyLifetimePrev = 0.00
	state.energy = 0.00
    
    return [
		zwave.meterV3.meterReset().format(),
		zwave.meterV3.meterGet(scale: 0).format()
	]
}


/**
 *  installed() - Runs when the device is first installed.
 **/
def installed() {
	log.debug "${device.displayName}: Installing."
	state.installedAt = now()
	state.energy = 0.00
	state.costPerKWH = 0.00
	state.costOfEnergy = 0.00
	state.lastReset = new Date().format("YYYY/MM/dd \n HH:mm:ss", location.timeZone)
    state.statsMode = 'Today'
}


/**
 *  updated() - Runs when you hit "Done" from "Edit Device".
 * 
 *  Weirdly, it seems to be called twice after hitting "Done"!
 * 
 *  Note, the updated() method is not a 'command', so it doesn't send commands by default.
 *  To execute commands from updated() you have to specifically return a HubAction object. 
 *  The response() helper wraps commands up in a HubAction so they can be sent from parse() or updated().
 *  See: https://community.smartthings.com/t/remotec-z-thermostat-configuration-with-z-wave-commands/31956/12
 **/
def updated() {

	log.debug "Updated() called"
	// Update internal state:
	state.debug = ("true" == configDebugMode)
	state.costPerKWH = configCostPerKWH as BigDecimal
    
    // Call configure() and refresh():
 	return response( [configure() , refresh() ])
}



/**
 *  updateStats() - Recalculates energy and cost for each reporting period.
 *
 *  All costs are calculated at the prevailing rate.
 *
 *   Attributes:
 *    energy                = Energy (kWh) as reported by device (ad hoc period). [Native Energy Meter attribute].
 *    costOfEnergy          = Cost of energy (ad hoc period).
 *    energyToday           = Accumulated energy (today only).
 *    costOfEnergyToday     = Cost of energy (today).
 *    energy24Hours         = Accumulated energy (last 24 hours).
 *    costOfEnergy24Hours   = Cost of energy (last 24 hours).
 *    energy7Days           = Accumulated energy (last 7 days).
 *    costOfEnergy7Days     = Cost of energy (last 7 days).
 *    energyMonth           = Accumulated energy (this month).
 *    costOfEnergyMonth     = Cost of energy (this month).
 *    energyYear            = Accumulated energy (this year).
 *    costOfEnergyYear      = Cost of energy (this year).
 *    energyLifetime        = Accumulated energy (lifetime).
 *    costOfEnergyLifetime  = Cost of energy (lifetime).
 *   
 *   Private State:
 *    costPerKWH            = Unit cost as specified by user in settings.
 *    reportingPeriod       = YYYY/MM/dd of current reporting period.
 *    energyTodayStart      = energy that was reported at the start of today. Will be zero if ad hoc period has been reset today.
 *    energyTodayPrev       = energy that was reported today, prior to lastReset. Will be zero if ad hoc period has not been reset today.
 *    energyMonthStart      = energy that was reported at the start of this month. Will be zero if ad hoc period has been reset this month.
 *    energyMonthPrev       = energy that was reported this month, prior to lastReset. Will be zero if ad hoc period has not been reset this month.
 *    energyYearStart       = energy that was reported at the start of this year. Will be zero if ad hoc period has been reset this year.
 *    energyYearPrev        = energy that was reported this year, prior to lastReset. Will be zero if ad hoc period has not been reset this year.
 *    energyLifetimePrev    = energy that was reported this lifetime, prior to lastReset. Will be zero if ad hoc period has never been reset.
 *   
 **/
private updateStats() {

	if (state.debug) log.debug "${device.displayName}: Updating Statistics"
	
	if (!state.energy) {state.energy = 0}
	if (!state.costPerKWH) {state.costPerKWH = 0}
	if (!state.reportingPeriod) {state.reportingPeriod = "Uninitialised"}
	if (!state.energyTodayStart) {state.energyTodayStart = 0}
	if (!state.energyTodayPrev) {state.energyTodayPrev = 0}
	if (!state.energyMonthStart) {state.energyMonthStart = 0}
	if (!state.energyMonthPrev) {state.energyMonthPrev = 0}
	if (!state.energyYearStart) {state.energyYearStart = 0}
	if (!state.energyYearPrev) {state.energyYearPrev = 0}
	if (!state.energyLifetimePrev) {state.energyLifetimePrev = 0}
	
	// Check if reportingPeriod has changed (i.e. it's a new day):
	def today = new Date().format("YYYY/MM/dd", location.timeZone)
	if ( today != state.reportingPeriod) {
		// It's a new Reporting Period:
		log.info "${device.displayName}: New Reporting Period: ${today}"
        
        // Check if new year:
		if ( today.substring(0,4) != state.reportingPeriod.substring(0,4)) {
        	state.energyYearStart = state.energy
			state.energyYearPrev = 0.00
        }

        // Check if new month:
		if ( today.substring(0,7) != state.reportingPeriod.substring(0,7)) {
        	state.energyMonthStart = state.energy
			state.energyMonthPrev = 0.00
        }

        // Daily rollover:
		state.energyTodayStart = state.energy
		state.energyTodayPrev = 0.00
        
        // Update reportingPeriod:
        state.reportingPeriod = today
	}
	
    // energy (ad hoc period):
    // Nothing to caclulate, just need to update dispEnergy:
    sendEvent(name: "dispEnergy", value: String.format("%.2f",state.energy as BigDecimal) + " kWh", displayed: false)
    
    // costOfEnergy (ad hoc period):
	try {
		state.costOfEnergy = state.energy * state.costPerKWH
		if (state.debug) log.debug "${device.displayName}: Cost of Energy: £${state.costOfEnergy}"
		sendEvent(name: "costOfEnergy", value: state.costOfEnergy, unit: "£")
        sendEvent(name: "dispCostOfEnergy", value: "£" + String.format("%.2f",state.costOfEnergy as BigDecimal), displayed: false)
	} catch (e) { log.debug e }

	// energyToday:
	try {
		state.energyToday = state.energy + state.energyTodayPrev - state.energyTodayStart
		if (state.debug) log.debug "${device.displayName}: Energy Today: ${state.energyToday} kWh"
		sendEvent(name: "energyToday", value: state.energyToday, unit: "kWh")
	} catch (e) { log.debug e }

	// costOfEnergyToday:
	try {
		state.costOfEnergyToday = (state.energyToday * state.costPerKWH) as BigDecimal
		if (state.debug) log.debug "${device.displayName}: Cost of Energy Today: £${state.costOfEnergyToday}"
		sendEvent(name: "costOfEnergyToday", value: state.costOfEnergyToday, unit: "£")
	} catch (e) { log.debug e }

	// energyMonth:
	try {
		state.energyMonth = state.energy + state.energyMonthPrev - state.energyMonthStart
		if (state.debug) log.debug "${device.displayName}: Energy This Month: ${state.energyMonth} kWh"
		sendEvent(name: "energyMonth", value: state.energyMonth, unit: "kWh")
	} catch (e) { log.debug e }

	// costOfEnergyMonth:
	try {
		state.costOfEnergyMonth = (state.energyMonth * state.costPerKWH) as BigDecimal
		if (state.debug) log.debug "${device.displayName}: Cost of Energy This Month: £${state.costOfEnergyMonth}"
		sendEvent(name: "costOfEnergyMonth", value: state.costOfEnergyMonth, unit: "£")
	} catch (e) { log.debug e }

	// energyYear:
	try {
		state.energyYear = state.energy + state.energyYearPrev - state.energyYearStart
		if (state.debug) log.debug "${device.displayName}: Energy This Year: ${state.energyYear} kWh"
		sendEvent(name: "energyYear", value: state.energyYear, unit: "kWh")
	} catch (e) { log.debug e }

	// costOfEnergyYear:
	try {
		state.costOfEnergyYear = (state.energyYear * state.costPerKWH) as BigDecimal
		if (state.debug) log.debug "${device.displayName}: Cost of Energy This Year: £${state.costOfEnergyYear}"
		sendEvent(name: "costOfEnergyYear", value: state.costOfEnergyYear, unit: "£")
	} catch (e) { log.debug e }

	// energyLifetime:
	try {
		state.energyLifetime = state.energy + state.energyLifetimePrev
		if (state.debug) log.debug "${device.displayName}: Energy This Lifetime: ${state.energyLifetime} kWh"
		sendEvent(name: "energyLifetime", value: state.energyLifetime, unit: "kWh")
	} catch (e) { log.debug e }

	// costOfEnergyLifetime:
	try {
		state.costOfEnergyLifetime = (state.energyLifetime * state.costPerKWH) as BigDecimal
		if (state.debug) log.debug "${device.displayName}: Cost of Energy This Lifetime: £${state.costOfEnergyLifetime}"
		sendEvent(name: "costOfEnergyLifetime", value: state.costOfEnergyLifetime, unit: "£")
	} catch (e) { log.debug e }
    
    // Moving Periods - Calculated by looking up previous values of energyLifetime:
    
    // energy24Hours:
	try {
    	// We need the last value of energyLifetime that is at least 24 hours old.
		//  We get previous values of energyLifetime between 1 and 7 days old, in case the device has been off for a while.
		//  So long as the device reported energy back at least once during this period, we should get a result.
        //  As results are returned in reverse chronological order, we just need the first 1 record.
		
        // Use a calendar object to create offset dates:
		Calendar cal = new GregorianCalendar()
		cal.add(Calendar.DATE, -1 )
		Date end = cal.getTime()
		cal.add(Calendar.DATE, -6 )
		Date start = cal.getTime()

		def previousELStates = device.statesBetween("energyLifetime", start, end,[max: 1])
		def previousEL
    	if (previousELStates) { 
        	previousEL = previousELStates[previousELStates.size -1].value as BigDecimal 
            if (state.debug) log.debug "${device.displayName}: energyLifetime 24 Hours Ago was: ${previousEL} kWh"
        }
    	else { 
        	previousEL = 0.0 
        	if (state.debug) log.debug "${device.displayName}: No value for energyLifetime 24 Hours Ago!"
        }
    	if (previousEL > state.energyLifetime) { previousEL = 0.0 } // If energyLifetime has been reset, discard previous value.
        
    	state.energy24Hours = state.energyLifetime - previousEL
        if (state.debug) log.debug "${device.displayName}: Energy Last 24 Hours: ${state.energy24Hours} kWh"
		sendEvent(name: "energy24Hours", value: state.energy24Hours, unit: "kWh")
	} catch (e) { log.debug e }    
    
	// costOfEnergy24Hours:
	try {
		state.costOfEnergy24Hours = (state.energy24Hours * state.costPerKWH) as BigDecimal
		if (state.debug) log.debug "${device.displayName}: Cost of Energy Last 24 Hours: £${state.costOfEnergy24Hours}"
		sendEvent(name: "costOfEnergy24Hours", value: state.costOfEnergy24Hours, unit: "£")
	} catch (e) { log.debug e }
    
    
    // energy7Days:
	try {
    	// We need the last value of energyLifetime, up to 7 old (previous states are only kept for 7 days).
		//  We get previous values of energyLifetime between 6 and 7 days old, in case the device has been off for a while.
		//  So long as the device reported energy back at least once during this period, we should get a result.
        //  As results are returned in reverse chronological order, we need the last record, so we request the max of 1000.
		//  If there were more than 1000 updates between start and end, we won't get the oldest one,
        //  however stats should normally only be generated every 10 mins at most.
		
    	// Use a calendar object to create offset dates:
		Calendar cal = new GregorianCalendar()
		cal.add(Calendar.DATE, -6 )
		Date end = cal.getTime()
		cal.add(Calendar.DATE, -1 )
		Date start = cal.getTime()

		// Get previous values of energyLifetime between 7 Days and 6 days 23 hours old: 
		def previousELStates = device.statesBetween("energyLifetime", start, end,[max: 1000])
		def previousEL
    	if (previousELStates) { 
        	previousEL = previousELStates[previousELStates.size -1].value as BigDecimal 
            if (state.debug) log.debug "${device.displayName}: energyLifetime 7 Days Ago was: ${previousEL} kWh"
        }
    	else { 
        	previousEL = 0.0 
        	if (state.debug) log.debug "${device.displayName}: No value for energyLifetime 7 Days Ago!"
        }
    	if (previousEL > state.energyLifetime) { previousEL = 0.0 } // If energyLifetime has been reset, discard previous value.
        
    	state.energy7Days = state.energyLifetime - previousEL
		if (state.debug) log.debug "${device.displayName}: Energy Last 7 Days: ${state.energy7Days} kWh"
		sendEvent(name: "energy7Days", value: state.energy7Days, unit: "kWh")
	} catch (e) { log.debug e }    
    
	// costOfEnergy7Days:
	try {
		state.costOfEnergy7Days = (state.energy7Days * state.costPerKWH) as BigDecimal
		if (state.debug) log.debug "${device.displayName}: Cost of Energy Last 7 Days: £${state.costOfEnergy7Days}"
		sendEvent(name: "costOfEnergy7Days", value: state.costOfEnergy7Days, unit: "£")
	} catch (e) { log.debug e }
    
    
    //disp<>Period:
    if ('Today' == state.statsMode) {
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energyToday as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergyToday as BigDecimal), displayed: false)
    }
    if ('Last 24 Hours' == state.statsMode) {
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energy24Hours as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergy24Hours as BigDecimal), displayed: false)
    }
    if ('Last 7 Days' == state.statsMode) {
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energy7Days as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergy7Days as BigDecimal), displayed: false)
    }
    if ('This Month' == state.statsMode) {
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energyMonth as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergyMonth as BigDecimal), displayed: false)
    }
    if ('This Year' == state.statsMode) {
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energyYear as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergyYear as BigDecimal), displayed: false)
    }
    if ('Lifetime' == state.statsMode) {
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energyLifetime as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergyLifetime as BigDecimal), displayed: false)
    }
    
}


/**
 *  cycleStats() - Cycle displayed statistics period.
 **/
def cycleStats() {
	if (state.debug) log.debug "$device.displayName: Cycling Stats"
	
    if ('Today' == state.statsMode) {
    	state.statsMode = 'Last 24 Hours'
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energy24Hours as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergy24Hours as BigDecimal), displayed: false)
    }
    else if ('Last 24 Hours' == state.statsMode) {
    	state.statsMode = 'Last 7 Days'
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energy7Days as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergy7Days as BigDecimal), displayed: false)
    }
    else if ('Last 7 Days' == state.statsMode) {
    	state.statsMode = 'This Month'
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energyMonth as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergyMonth as BigDecimal), displayed: false)
    }
    else if ('This Month' == state.statsMode) {
    	state.statsMode = 'This Year'
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energyYear as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergyYear as BigDecimal), displayed: false)
    }
    else if ('This Year' == state.statsMode) {
    	state.statsMode = 'Lifetime'
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energyLifetime as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergyLifetime as BigDecimal), displayed: false)
    }
    else  {
    	state.statsMode = 'Today'
    	sendEvent(name: "dispEnergyPeriod", value: String.format("%.2f",state.energyToday as BigDecimal) + " kWh", displayed: false)
		sendEvent(name: "dispCostOfEnergyPeriod", value: "£" + String.format("%.2f",state.costOfEnergyToday as BigDecimal), displayed: false)
    }
    
	sendEvent(name: "statsMode", value: state.statsMode, displayed: false)
	if (state.debug) log.debug "$device.displayName: StatsMode changed to: ${state.statsMode}"
	
}


/**
 *  configure() - Configure physical device parameters.
 *
 *  Gets values from the Preferences section.
 **/
def configure() {
    
    if (state.debug) log.debug "$device.displayName: Configuring Device"
    
    // Build Commands based on input preferences:
    // Some basic validation is done, if any values are out of range they're set back to default.
    //  It doesn't seem possible to read the defaultValue of each input from $settings, so default values are duplicated here.
    def cmds = []
    
	// Parameter 2 - Energy Detection Mode:
	Short CP2 
    if (configEnergyDetectionMode == "Wattage, absolute kWh") {CP2 = 0}
	else if (configEnergyDetectionMode == "+/-Wattage, algebraic sum kWh") {CP2 = 1}
	else if (configEnergyDetectionMode == "+/-Wattage, +ive kWh (consuming electricity)") {CP2 = 2}
	else if (configEnergyDetectionMode == "+/-Wattage, -iv kWh (generating electricity)") {CP2 = 3}
	else {CP2 = 0}
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: CP2).format() 
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format()
	
	// Parameter 3 - Selective Reporting:
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: ("true" == configSelectiveReporting) ? 1 : 0).format()
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format()		
	
    // Parameter 4 - Power Change Threshold for Auto-Report - Whole HEM (W):
	Long CP4 = settings.configPowerThresholdAbs_HEM as Long  
    if ((CP4 == null) || (CP4 < 0) || (CP4 > 60000)) { CP4 = 50 }
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 2, scaledConfigurationValue: CP4).format()
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 4).format()

    // Parameter 8 - Power Percentage Change Threshold for Auto-Report - Whole HEM (%):
    Long CP8 = settings.configPowerThresholdPercent_HEM as Long  
    if ((CP8 == null) || (CP8 < 0) || (CP8 > 100)) { CP8 = 10 }
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 8, size: 1, scaledConfigurationValue: CP8).format()
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 8).format()

	// Reporting Group Flags:
	//  energy = 1
	//  power = 2
	//  voltage = 4
	//  current = 8
        
    // Parameter 101 - Reporting Group 1 - Content Flags:
	// HARD-CODED to contain power (W) and current [2+8 = 10]:
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 10).format()
	cmds << zwave.configurationV1.configurationGet(parameterNumber: 101).format()
	
	// Parameter 102 - Reporting Group 2 - Content Flags:
	// HARD-CODED to contain energy [1]:
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1).format()
	cmds << zwave.configurationV1.configurationGet(parameterNumber: 102).format()
	
	// Parameter 103 - Reporting Group 3 - Content Flags:
	// HARD-CODED to contain voltage [4]:
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 4).format()
	cmds << zwave.configurationV1.configurationGet(parameterNumber: 103).format()
	
	
	// Parameter 111 - Reporting Group 1 - Report Interval (s):
	Long CP111 = settings.configReportGroup1Interval as Long  
    if ((CP111 == null) || (CP111 < 1) || (CP111 > 2147483647)) { CP111 = 60 }
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: CP111).format()
	cmds << zwave.configurationV1.configurationGet(parameterNumber: 111).format()
	
	// Parameter 112 - Reporting Group 2 - Report Interval (s):
	Long CP112 = settings.configReportGroup2Interval as Long  
    if ((CP112 == null) || (CP112 < 1) || (CP112 > 2147483647)) { CP112 = 600 }
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: CP112).format()
	cmds << zwave.configurationV1.configurationGet(parameterNumber: 112).format()
	
	// Parameter 113 - Reporting Group 3 - Report Interval (s):
	Long CP113 = settings.configReportGroup3Interval as Long  
    if ((CP113 == null) || (CP113 < 1) || (CP113 > 2147483647)) { CP113 = 600 }
	cmds << zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: CP113).format()
	cmds << zwave.configurationV1.configurationGet(parameterNumber: 113).format()
	
	
    // Return:
    if ( cmds != [] && cmds != null ) return delayBetween(cmds, 500) else return
}


/**
 *  test() - Temp testing method.
 **/
def test() {
	if (state.debug) log.debug "${device.displayName}: Testing"
}



================================================
FILE: devices/evohome/evohome-heating-zone.groovy
================================================
/**
 *  Copyright 2016 David Lomas (codersaur)
 *
 *  Name: Evohome Heating Zone
 *
 *  Author: David Lomas (codersaur)
 *
 *  Date: 2016-04-08
 *
 *  Version: 0.09
 *
 *  Description:
 *   - This device handler is a child device for the Evohome (Connect) SmartApp.
 *   - For latest documentation see: https://github.com/codersaur/SmartThings
 *
 *  Version History:
 *
 *   2016-04-08: v0.09
 *    - calculateOptimisations(): Fixed comparison of temperature values.
 * 
 *   2016-04-05: v0.08
 *    - New 'Update Refresh Time' setting from parent to control polling after making an update.
 *    - setThermostatMode(): Forces poll for all zones to ensure new thermostatMode is updated.
 * 
 *   2016-04-04: v0.07
 *    - generateEvent(): hides events if name or value are null.
 *    - generateEvent(): log.info message for new values.
 * 
 *   2016-04-03: v0.06
 *    - Initial Beta Release
 * 
 *  To Do:
 *   - Clean up device settings (preferences). Hide/Show prefSetpointDuration input dynamically depending on prefSetpointMode. - If supported for devices???
 *   - When thermostat mode is away or off, heatingSetpoint overrides should not allowed (although setting while away actually works). Should warn at least.
 *
 *  License:
 *   Licensed 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:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless 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.
 *
 */ 
metadata {
	definition (name: "Evohome Heating Zone", namespace: "codersaur", author: "David Lomas") {
		capability "Actuator"
		capability "Sensor"
		capability "Refresh"
		capability "Temperature Measurement"
		capability "Thermostat"
		
		//command "poll" // Polling
		command "refresh" // Refresh
		command "setHeatingSetpoint" // Thermostat
		command "raiseSetpoint" // Custom
		command "lowerSetpoint" // Custom
		command "setThermostatMode" // Thermostat
		command "cycleThermostatMode" // Custom
		command "off" // Thermostat
		command "heat" // Thermostat
		command "auto" // Custom
		command "away" // Custom
		command "economy" // Custom
		command "dayOff" // Custom
		command "custom" // Custom
		command "resume" // Custom
		command "boost" // Custom
		command "suppress" // Custom
		command "generateEvent" // Custom
		command "test" // Custom

		attribute "temperature","number" // Temperature Measurement
		attribute "heatingSetpoint","number" // Thermostat
		attribute "thermostatSetpoint","number" // Thermostat
		attribute "thermostatSetpointMode", "string" // Custom
		attribute "thermostatSetpointUntil", "string" // Custom
		attribute "thermostatSetpointStatus", "string" // Custom
		attribute "thermostatMode", "string" // Thermostat
		attribute "thermostatOperatingState", "string" // Thermostat
		attribute "thermostatStatus", "string" // Custom
		attribute "scheduledSetpoint", "number" // Custom
		attribute "nextScheduledSetpoint", "number" // Custom
		attribute "nextScheduledTime", "string" // Custom
		attribute "optimisation", "string" // Custom
		attribute "windowFunction", "string" // Custom
		
	}

	tiles(scale: 2) {

		// Main multi
		multiAttributeTile(name:"multi", type:"thermostat", width:6, height:4) {
			tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
				attributeState("default", label:'${currentValue}', unit:"C")
			}
			// Up and Down buttons:
			//tileAttribute("device.temperature", key: "VALUE_CONTROL") {
			//	attributeState("VALUE_UP", action: "raiseSetpoint")
			//	attributeState("VALUE_DOWN", action: "lowerSetpoint")
			//}
			// Operating State - used to get background colour when type is 'thermostat'.
			tileAttribute("device.thermostatStatus", key: "OPERATING_STATE") {
				attributeState("Heating", backgroundColor:"#ffa81e", defaultState: true)
				attributeState("Idle (Auto)", backgroundColor:"#44b621")
				attributeState("Idle (Custom)", backgroundColor:"#44b621")
				attributeState("Idle (Day Off)", backgroundColor:"#44b621")
				attributeState("Idle (Economy)", backgroundColor:"#44b621")
				attributeState("Idle (Away)", backgroundColor:"#44b621")
				attributeState("Off", backgroundColor:"#269bd2")
			}
			//tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
			//	attributeState("off", label:'${name}')
			//	attributeState("away", label:'${name}')
			//	attributeState("auto", label:'${name}')
			//	attributeState("economy", label:'${name}')
			//	attributeState("dayOff", label:'${name}')
			//	attributeState("custom", label:'${name}')
			//}
			//tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
			//	attributeState("default", label:'${currentValue}', unit:"C")
			//}
			//tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
			//	attributeState("default", label:'${currentValue}', unit:"C")
			//}
		}
	
		// temperature tile:
		valueTile("temperature", "device.temperature", width: 2, height: 2, canChangeIcon: true) {
			state("temperature", label:'${currentValue}', unit:"C", icon:"st.Weather.weather2",
					backgroundColors:[
							// Celsius
							[value: 0, color: "#153591"],
							[value: 7, color: "#1e9cbb"],
							[value: 15, color: "#90d2a7"],
							[value: 23, color: "#44b621"],
							[value: 28, color: "#f1d801"],
							[value: 35, color: "#d04e00"],
							[value: 37, color: "#bc2323"]
					]
			)
		}
		
		// thermostatSetpoint tiles:
		valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 3, height: 1) {
			state "thermostatSetpoint", label:'Setpoint: ${currentValue}', unit:"C"
		}
		valueTile("thermostatSetpointStatus", "device.thermostatSetpointStatus", width: 3, height: 1, decoration: "flat") {
			state "thermostatSetpointStatus", label:'${currentValue}', backgroundColor:"#ffffff"
		}
		standardTile("raiseSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
			state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
		}
		standardTile("lowerSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
			state "setpoint", action:"lowerSetpoint", icon:"st.thermostat.thermostat-down"
		}
		standardTile("resume", "device.resume", width: 1, height: 1, decoration: "flat") {
			state "default", action:"resume", label:'Resume', icon:"st.samsung.da.oven_ic_send"
		}
		standardTile("boost", "device.boost", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
			state "default", action:"boost", label:'Boost' // icon TBC
		}
		standardTile("suppress", "device.suppress", inactiveLabel: false, decoration: "flat", width: 1, height: 1) {
			state "default", action:"suppress", label:'Suppress' // icon TBC
		}
		
		
		// thermostatMode/Status Tiles:
		
		// thermostatStatus (also incorporated into the multi tile).
		valueTile("thermostatStatus", "device.thermostatStatus", height: 1, width: 6, decoration: "flat") {
			state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
		}
		// Single thermostatMode tile that cycles between all modes (too slow).
		// To Do: Update with Evohome-specific modes:
		standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
			state "off", action:"cycleMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off"
			state "heat", action:"cycleMode",  nextState: "updating", icon: "st.thermostat.heat"
			state "cool", action:"cycleMode",  nextState: "updating", icon: "st.thermostat.cool"
			state "auto", action:"cycleMode",  nextState: "updating", icon: "st.thermostat.auto"
			state "auxHeatOnly", action:"cycleMode", icon: "st.thermostat.emergency-heat"
			state "updating", label:"Working", icon: "st.secondary.secondary"
		}
		// Individual Mode tiles:
		standardTile("auto", "device.auto", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", action:"auto", icon: "st.thermostat.auto"
		}
		standardTile("away", "device.away", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", action:"away", label:'Away' // icon TBC
		}
		standardTile("custom", "device.custom", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", action:"custom", label:'Custom' // icon TBC
		}
		standardTile("dayOff", "device.dayOff", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", action:"dayOff", label:'Day Off' // icon TBC
		}
		standardTile("economy", "device.economy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", action:"economy", label:'Economy' // icon TBC
		}
		standardTile("off", "device.off", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
			state "default", action:"off", icon:"st.thermostat.heating-cooling-off"
		}
		// Other tiles:
		standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
			state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
		}
		standardTile("test", "device.test", width: 1, height: 1, decoration: "flat") {
			state "default", label:'Test', action:"test"
		}
		
		main "temperature"
		details(
				[
				"multi",
				"thermostatSetpoint","raiseSetpoint","boost","resume",
				"thermostatSetpointStatus","lowerSetpoint","suppress","refresh",
				"auto","away","custom","dayOff","economy","off"
				]
		)
	}

	preferences {
		section { // Setpoint Adjustments:
			input title: "Setpoint Duration", description: "Configure how long setpoint adjustments are applied for.", displayDuringSetup: true, type: "paragraph", element: "paragraph"
			input 'prefSetpointMode', 'enum', title: 'Until', description: '', options: ["Next Switchpoint", "Midday", "Midnight", "Duration", "Permanent"], defaultValue: "Next Switchpoint", required: true, displayDuringSetup: true
			input 'prefSetpointDuration', 'number', title: 'Duration (minutes)', description: 'Apply setpoint for this many minutes', range: "1..1440", defaultValue: 60, required: true, displayDuringSetup: true
			//input 'prefSetpointTime', 'time', title: 'Time', description: 'Apply setpoint until this time', required: true, displayDuringSetup: true
			input title: "Setpoint Temperatures", description: "Configure preset temperatures for the 'Boost' and 'Suppress' buttons.", displayDuringSetup: true, type: "paragraph", element: "paragraph"
			input "prefBoostTemperature", "string", title: "'Boost' Temperature", defaultValue: "21.5", required: true, displayDuringSetup: true // use of 'decimal' input type in devices is currently broken.
			input "prefSuppressTemperature", "string", title: "'Suppress' Temperature", defaultValue: "15.0", required: true, displayDuringSetup: true // use of 'decimal' input type in devices is currently broken.
		}
				
	}

}

/**********************************************************************
 *  Test Commands:
 **********************************************************************/


/**
 *  test()
 *
 *  Test method, called from test tile.
 **/
def test() {

	//log.debug "$device.displayName: test(): Properties: ${properties}"
	//log.debug "$device.displayName: test(): Settings: ${settings}"
	//log.debug "$device.displayName: test(): State: ${state}"    

}


/**********************************************************************
 *  Setup and Configuration Commands:
 **********************************************************************/

/**
 *  installed()
 *
 *  Runs when the app is first installed.
 *  
 *  When a device is created by a SmartApp, settings are not populated
 *  with the defaultValues configured for each input. Therefore, we
 *  populate the corresponding state.* variables with the input defaultValues.
 * 
 **/
def installed() {

	log.debug "${app.label}: Installed with settings: ${settings}"

	state.installedAt = now()
	
	// These default values will be overwritten by the Evohome SmartApp almost immediately:
	state.debug = false
    state.updateRefreshTime = 5 // Wait this many seconds after an update before polling.
	state.zoneType = 'RadiatorZone'
	state.minHeatingSetpoint = formatTemperature(5.0)
	state.maxHeatingSetpoint = formatTemperature(35.0)
	state.temperatureResolution = formatTemperature(0.5)
	state.windowFunctionTemperature = formatTemperature(5.0)
	state.targetSetpoint = state.minHeatingSetpoint
	
	// Populate state.* with default values for each preference/input:
	state.setpointMode = getInputDefaultValue('prefSetpointMode')
	state.setpointDuration = getInputDefaultValue('prefSetpointDuration')
	state.boostTemperature = getInputDefaultValue('prefBoostTemperature')
	state.suppressTemperature = getInputDefaultValue('prefSuppressTemperature')
	
}


/**
 *  updated()
 * 
 *  Runs when device settings are changed.
 **/
def updated() {

	if (state.debug) log.debug "${device.label}: Updating with settings: ${settings}"

	// Copy input values to state:
	state.setpointMode = settings.prefSetpointMode
	state.setpointDuration = settings.prefSetpointDuration
	state.boostTemperature = formatTemperature(settings.prefBoostTemperature)
	state.suppressTemperature = formatTemperature(settings.prefSuppressTemperature)

}


/**********************************************************************
 *  SmartApp-Child Interface Commands:
 **********************************************************************/

/**
 *  generateEvent(values)
 *
 *  Called by parent to update the state of this child device.
 *
 **/
void generateEvent(values) {

	log.info "${device.label}: generateEvent(): New values: ${values}"
	
	if(values) {
		values.each { name, value ->
			if ( name == 'minHeatingSetpoint' 
				|| name == 'maxHeatingSetpoint' 
				|| name == 'temperatureResolution' 
				|| name == 'windowFunctionTemperature'
				|| name == 'zoneType'
				|| name == 'locationId'
				|| name == 'gatewayId'
				|| name == 'systemId'
				|| name == 'zoneId'
				|| name == 'schedule'
				|| name == 'debug'
                || name == 'updateRefreshTime'
				) {
				// Internal state only.
				state."${name}" = value
			}
			else { // Attribute value, so generate an event:
				if (name != null && value != null) {
					sendEvent(name: name, value: value, displayed: true)
				}
				else { // If name or value is null, set displayed to false,
					   // otherwise the 'Recently' view on smartphone app clogs 
					   // up with empty events.
					sendEvent(name: name, value: value, displayed: false)
				}
				
				// Reset targetSetpoint (used by raiseSetpoint/lowerSetpoint) if heatingSetpoint has changed:
				if (name == 'heatingSetpoint') {
					state.targetSetpoint = value
				}
			}
		}
	}
	
	// Calculate derived attributes (order is important here):
	calculateThermostatOperatingState()
	calculateOptimisations()
	calculateThermostatStatus()
	calculateThermostatSetpointStatus()
	
}


/**********************************************************************
 *  Capability-related Commands:
 **********************************************************************/


/**
 *  poll()
 *
 *  Polls the device. Required for the "Polling" capability
 **/
void poll() {

	if (state.debug) log.debug "${device.label}: poll()"
	parent.poll(state.zoneId)
}


/**
 *  refresh()
 *
 *  Refreshes values from the device. Required for the "Refresh" capability.
 **/
void refresh() {

	if (state.debug) log.debug "${device.label}: refresh()"
	sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
	parent.poll(state.zoneId)
}


/**
 *  setThermostatMode(mode, until=-1)
 * 
 *  Set thermostat mode until specified time.
 *
 *   mode:    Possible values: 'auto','off','away','dayOff','custom', or 'economy'.
 *
 *   until:   (Optional) Time to apply mode until, can be either:
 *             - Date: Date object representing when override should end.
 *             - ISO-8601 date string, in format "yyyy-MM-dd'T'HH:mm:ssXX", e.g.: "2016-04-01T00:00:00Z".
 *             - String: 'permanent'.
 *             - Number: Duration in hours if mode is 'economy', or days if mode is 'away'/'dayOff'/'custom'.
 *                       Duration will be rounded down to align with Midnight i nthe local timezone
 *                       (e.g. a duration of 1 day will end at midnight tonight). If 0, mode is permanent.
 *                       If duration is not specified, a default value is used from the Evohome SmartApp settings.
 *
 *   Notes:   'Auto' and 'Off' modes are always permanent.
 *            Thermostat mode is a property of the temperatureControlSystem (i.e. Evohome controller).
 *            Therefore changing the thermostatMode will affect all zones associated with the same controller.
 * 
 *  Example usage:
 *   setThermostatMode('off', 0)         // Set off mode permanently.
 *   setThermostatMode('away', 1)        // Set away mode for one day (i.e. until midnight tonight).
 *   setThermostatMode('dayOff', 2)      // Set dayOff mode for two days (ends tomorrow night).
 *   setThermostatMode('economy', 2)     // Set economy mode for two hours.
 *
 **/
def setThermostatMode(String mode, until=-1) {

	log.info "${device.label}: setThermostatMode(Mode: ${mode}, Until: ${until})"
	
	// Send update via parent:
	if (!parent.setThermostatMode(state.systemId, mode, until)) {
		sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
		// Wait a few seconds as it takes a while for Evohome to update setpoints in response to a mode change.
		pseudoSleep(state.updateRefreshTime * 1000)
		parent.poll(0) // Force poll for all zones as thermostatMode is a property of the temperatureControlSystem.
		return null
	}
	else {
		log.error "${device.label}: setThermostatMode(): Error: Unable to set thermostat mode."
		return 'error'
	}
}


/**
 *  setHeatingSetpoint(setpoint, until=-1)
 * 
 *  Set heatingSetpoint until specified time.
 *
 *   setpoint:   Setpoint temperature, e.g.: "21.5". Can be a number or string.
 *               If setpoint is outside allowed range (i.e. minHeatingSetpoint to 
 *               maxHeatingSetpoint) it will be re-written to the appropriate limit.
 *
 *   until:      (Optional) Time to apply setpoint until, can be either:
 *                - Date: date object representing when override should end.
 *                - ISO-8601 date string, in format "yyyy-MM-dd'T'HH:mm:ssXX", e.g.: "2016-04-01T00:00:00Z".
 *                - String: 'nextSwitchpoint', 'midnight', 'midday', or 'permanent'.
 *                - Number: duration in minutes (from now). 0 = permanent.
 *               If not specified, setpoint duration will default to the
 *               behaviour defined in the device settings.
 *
 *  Example usage:
 *   setHeatingSetpoint(21.0)                           // Set until <device default>.
 *   setHeatingSetpoint(21.0, 'nextSwitchpoint')        // Set until next scheduled switchpoint.
 *   setHeatingSetpoint(21.0, 'midnight')               // Set until midnight.
 *   setHeatingSetpoint(21.0, 'permanent')              // Set permanently.
 *   setHeatingSetpoint(21.0, 0)                        // Set permanently.
 *   setHeatingSetpoint(21.0, 6)                        // Set for 6 hours.
 *   setHeatingSetpoint(21.0, '2016-04-01T00:00:00Z')   // Set until specific time.
 *
 **/
def setHeatingSetpoint(setpoint, until=-1) {

	if (state.debug) log.debug "${device.label}: setHeatingSetpoint(Setpoint: ${setpoint}, Until: ${until})"
	
	// Clean setpoint:
	setpoint = formatTemperature(setpoint)
	if (Float.parseFloat(setpoint) < Float.parseFloat(state.minHeatingSetpoint)) {
		log.warn "${device.label}: setHeatingSetpoint(): Specified setpoint (${setpoint}) is less than zone's minimum setpoint (${state.minHeatingSetpoint})."
		setpoint = state.minHeatingSetpoint
	}
	else if (Float.parseFloat(setpoint) > Float.parseFloat(state.maxHeatingSetpoint)) {
		log.warn "${device.label}: setHeatingSetpoint(): Specified setpoint (${setpoint}) is greater than zone's maximum setpoint (${state.maxHeatingSetpoint})."
		setpoint = state.maxHeatingSetpoint
	}
	
	// Clean and parse until value:
	def untilRes
	Calendar c = new GregorianCalendar()
	def tzOffset = location.timeZone.getOffset(new Date().getTime()) // Timezone offset to UTC in milliseconds.
	
	// If until has not been specified, determine behaviour from device state.setpointMode:
	if (-1 == until) {
		switch (state.setpointMode) {
	    	case 'Next Switchpoint':
	        	until = 'nextSwitchpoint'
	            break
	    	case 'Midday':
	        	until = 'midday'
	            break
	    	case 'Midnight':
	        	until = 'midnight'
	            break
	    	case 'Duration':
	        	until = state.setpointDuration ?: 0
	            break
	    	case 'Time':
				// TO DO : construct time, like we do for midnight.
				// settings.prefSetpointTime appears to return an ISO dateformat string.
				// However using an input of type "time" causes HTTP 500 errors in the IDE, so disabled for now.
				// If time has passed, then need to make it the next day.
				if (state.debug) log.debug "${device.label}: setHeatingSetpoint(): Time: ${state.SetpointTime}"
	        	until = 'nextSwitchpoint'
	            break
	    	case 'Permanent':
	        	until = 'permanent'
	            break
	    	default:
	        	until = 'nextSwitchpoint'
	            break
		}
	}
	
	if ('permanent' == until || 0 == until) {
		untilRes = 0
	}
	else if (until instanceof Date) {
		untilRes = until
	}
	else if ('nextSwitchpoint' == until) {
		untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", device.currentValue('nextScheduledTime'))
	}
	else if ('midday' == until) {
		untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", new Date().format("yyyy-MM-dd'T'12:00:00XX", location.timeZone)) 
	}
	else if ('midnight' == until) {
		c.add(Calendar.DATE, 1 ) // Add one day to calendar and use to get midnight in local time:
		untilRes =  new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", c.getTime().format("yyyy-MM-dd'T'00:00:00XX", location.timeZone))
	}
	else if (until ==~ /\d+.*T.*/) { // until is a ISO-8601 date string, so parse:
		untilRes = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", until)
	}
	else if (until.isNumber()) { // until is a duration in minutes, so construct date from now():
		// Evohome supposedly only accepts setpoints for up to 24 hours, so we should limit minutes to 1440.
		// For now, just pass any duration and see if Evohome accepts it...
		untilRes = new Date( now() + (Math.round(until) * 60000) )
	}
	else {
		log.warn "${device.label}: setHeatingSetpoint(): until value could not be parsed. Setpoint will be applied permanently."
		untilRes = 0
	}
	
	log.info "${device.label}: setHeatingSetpoint(): Setting setpoint to: ${setpoint} until: ${untilRes}"
	
	// Send update via parent:
	if (!parent.setHeatingSetpoint(state.zoneId, setpoint, untilRes)) {
		// Command was successful, but it takes a few seconds for the Evohome cloud service to update with new values.
		// Meanwhile, we know the new setpoint and thermostatSetpointMode anyway:
		sendEvent(name: 'heatingSetpoint', value: setpoint)
		sendEvent(name: 'thermostatSetpoint', value: setpoint)
		sendEvent(name: 'thermostatSetpointMode', value: (0 == untilRes) ? 'permanentOverride' : 'temporaryOverride' )
		sendEvent(name: 'thermostatSetpointUntil', value: (0 == untilRes) ? null : untilRes.format("yyyy-MM-dd'T'HH:mm:00XX", TimeZone.getTimeZone('UTC')))
		calculateThermostatOperatingState()
		calculateOptimisations()
		calculateThermostatStatus()
		sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
		pseudoSleep(state.updateRefreshTime * 1000)
		parent.poll(state.zoneId)
		return null
	}
	else {
		log.error "${device.label}: setHeatingSetpoint(): Error: Unable to set heating setpoint."
		return 'error'
	}
}



/**
 *  clearHeatingSetpoint()
 * 
 *  Clear the heatingSetpoint. Will return heatingSetpoint to scheduled value.
 *  thermostatSetpointMode should return to "followSchedule".
 * 
 **/
def clearHeatingSetpoint() {

	log.info "${device.label}: clearHeatingSetpoint()"

	// Send update via parent:
	if (!parent.clearHeatingSetpoint(state.zoneId)) {
		// Command was successful, but it takes a few seconds for the Evohome cloud service
		// to update the zone status with the new heatingSetpoint.
		// Meanwhile, we know the new thermostatSetpointMode is "followSchedule".
		sendEvent(name: 'thermostatSetpointMode', value: 'followSchedule')
		sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
		// sleep command is not allowed in SmartThings, so we use psuedoSleep().
		pseudoSleep(state.updateRefreshTime * 1000)
		parent.poll(state.zoneId)
		return null
	}
	else {
		log.error "${device.label}: clearHeatingSetpoint(): Error: Unable to clear heating setpoint."
		return 'error'
	}
}


/**
 *  raiseSetpoint()
 * 
 *  Raise heatingSetpoint and thermostatSetpoint.
 *  Increments by state.temperatureResolution (usually 0.5).
 *
 *  Called by raiseSetpoint tile.
 * 
 **/
void raiseSetpoint() {

	if (state.debug) log.debug "${device.label}: raiseSetpoint()"
	
	def mode = device.currentValue("thermostatMode")
	def targetSp = new BigDecimal(state.targetSetpoint)
	def tempRes = new BigDecimal(state.temperatureResolution) // (normally 0.5)
	def maxSp = new BigDecimal(state.maxHeatingSetpoint)
	
	if ('off' == mode || 'away' == mode) {
		log.warn "${device.label}: raiseSetpoint(): thermostat mode (${mode}) does not allow altering the temperature setpoint."
	}
	else {
		targetSp += tempRes

		if (targetSp > maxSp) {
			targetSp = maxSp
		}
		
		state.targetSetpoint = targetSp
		log.info "${device.label}: raiseSetpoint(): Target setpoint raised to: ${targetSp}"
		sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
		runIn(3, "alterSetpoint", [overwrite: true]) // Wait three seconds in case targetSetpoint is changed again.
	}
	
}


/**
 *  lowerSetpoint()
 * 
 *  Lower heatingSetpoint and thermostatSetpoint.
 *  Increments by state.temperatureResolution (usually 0.5).
 *
 *  Called by lowerSetpoint tile.
 * 
 **/
void lowerSetpoint() {

	if (state.debug) log.debug "${device.label}: lowerSetpoint()"
	
	def mode = device.currentValue("thermostatMode")
	def targetSp = new BigDecimal(state.targetSetpoint)
	def tempRes = new BigDecimal(state.temperatureResolution) // (normally 0.5)
	def minSp = new BigDecimal(state.minHeatingSetpoint)
	
	if ('off' == mode || 'away' == mode) {
		log.warn "${device.label}: lowerSetpoint(): thermostat mode (${mode}) does not allow altering the temperature setpoint."
	}
	else {
		targetSp -= tempRes 

		if (targetSp < minSp) {
			targetSp = minSp
		}
		
		state.targetSetpoint = targetSp
		log.info "${device.label}: lowerSetpoint(): Target setpoint lowered to: ${targetSp}"
		sendEvent(name: 'thermostatSetpointStatus', value: 'Updating', displayed: false)
		runIn(3, "alterSetpoint", [overwrite: true]) // Wait three seconds in case targetSetpoint is changed again.
	}
	
}


/**
 *  alterSetpoint()
 * 
 *  Proxy command called by raiseSetpoint and lowerSetpoint, as runIn 
 *  cannot pass targetSetpoint diretly to setHeatingSetpoint.
 *
 **/
private alterSetpoint() {

	if (state.debug) log.debug "${device.label}: alterSetpoint()"
	
	setHeatingSetpoint(state.targetSetpoint)
}


/**********************************************************************
 *  Convenience Commands:
 *   These commands alias other commands with preset parameters.
 **********************************************************************/

void resume() {
	if (state.debug) log.debug "${device.label}: resume()"
	clearHeatingSetpoint()
}

void auto() {
	if (state.debug) log.debug "${device.label}: auto()"
	setThermostatMode('auto')
}

void heat() {
	if (state.debug) log.debug "${device.label}: heat()"
	setThermostatMode('auto')
}

void off() {
	if (state.debug) log.debug "${device.label}: off()"
	setThermostatMode('off')
}

void away(until=-1) {
	if (state.debug) log.debug "${device.label}: away()"
	setThermostatMode('away', until)
}

void custom(until=-1) {
	if (state.debug) log.debug "${device.label}: custom()"
	setThermostatMode('custom', until)
}

void dayOff(until=-1) {
	if (state.debug) log.debug "${device.label}: dayOff()"
	setThermostatMode('dayOff', until)
}

void economy(until=-1) {
	if (state.debug) log.debug "${device.label}: economy()"
	setThermostatMode('economy', until)
}

void boost() {
	if (state.debug) log.debug "${device.label}: boost()"
	setHeatingSetpoint(state.boostTemperature)
}

void suppress() {
	if (state.debug) log.debug "${device.label}: suppress()"
	setHeatingSetpoint(state.suppressTemperature)
}

/**********************************************************************
 *  Helper Commands:
 **********************************************************************/

/**
 *  pseudoSleep(ms)
 * 
 *  Substitute for sleep() command.
 *
 **/
private pseudoSleep(ms) {
	def start = now()
	while (now() < start + ms) {
		// Do nothing, just wait.
	}
}


/**
 *  getInputDefaultValue(inputName)
 * 
 *  Get the default value for the specified input.
 *
 **/
private getInputDefaultValue(inputName) {

	if (state.debug) log.debug "${device.label}: getInputDefaultValue()"
	
	def returnValue
	properties.preferences?.sections.each { section ->
		section.input.each { input ->
			if (input.name == inputName) {
				returnValue = input.defaultValue
			}
		}
	}
	
	return returnValue
}



/**
 *  formatTemperature(t)
 * 
 *  Format temperature value to one decimal place.
 *  t:   can be string, float, bigdecimal...
 *  Returns as string.
 **/
private formatTemperature(t) {
	//return Float.parseFloat("${t}").round(1)
	//return String.format("%.1f", Float.parseFloat("${t}").round(1))
	return Float.parseFloat("${t}").round(1).toString()
}


/**
 *  formatThermostatModeForDisp(mode)
 * 
 *  Translate SmartThings values to display values.
 *   
 **/
private formatThermostatModeForDisp(mode) {

	if (state.debug) log.debug "${device.label}: formatThermostatModeForDisp()"

	switch (mode) {
		case 'auto':
			mode = 'Auto'
			break
		case 'economy':
			mode = 'Economy'
			break
		case 'away':
			mode = 'Away'
			break
		case 'custom':
			mode = 'Custom'
			break
		case 'dayOff':
			mode = 'Day Off'
			break
		case 'off':
			mode = 'Off'
			break
		default:
			mode = 'Unknown'
			break
	}

	return mode
 }
  

/**
 *  calculateThermostatOperatingState()
 * 
 *  Calculates thermostatOperatingState and generates event accordingly.
 *
 **/
private calculateThermostatOperatingState() {

	if (state.debug) log.debug "${device.label}: calculateThermostatOperatingState()"

	def tOS
	if ('off' == device.currentValue('thermostatMode')) {
		tOS = 'off'
	}
	else if (device.currentValue("temperature") < device.currentValue("thermostatSetpoint")) {
		tOS = 'heating'
	}
	else {
		tOS = 'idle'
	}
	
	sendEvent(name: 'thermostatOperatingState', value: tOS)
}


/**
 *  calculateOptimisations()
 * 
 *  Calculates if optimisation and windowFunction are active 
 *  and generates events accordingly.
 *
 *  This isn't going to be 100% perfect, but is reasonably accurate.
 *
 **/
private calculateOptimisations() {

	if (state.debug) log.debug "${device.label}: calculateOptimisations()"

	def newOptValue = 'inactive'
	def newWdfValue = 'inactive'
	
    // Convert temp values to BigDecimals for comparison:
	def heatingSp = new BigDecimal(device.currentValue('heatingSetpoint'))
	def scheduledSp = new BigDecimal(device.currentValue('scheduledSetpoint'))
	def nextScheduledSp = new BigDecimal(device.currentValue('nextScheduledSetpoint'))
	def windowTemp = new BigDecimal(state.windowFunctionTemperature)
    
	if ('auto' != device.currentValue('thermostatMode')) {
		// Optimisations cannot be active if thermostatMode is not 'auto'.
	}
	else if ('followSchedule' != device.currentValue('thermostatSetpointMode')) {
		// Optimisations cannot be active if thermostatSetpointMode is not 'followSchedule'.
		// There must be a manual override.
	}
	else if (heatingSp == scheduledSp) {
		// heatingSetpoint is what it should be, so no reason to suspect that optimisations are active.
	}
	else if (heatingSp == nextScheduledSp) {
		// heatingSetpoint is the nextScheduledSetpoint, so optimisation is likely active:
		newOptValue = 'active'
	}
	else if (heatingSp == windowTemp) {
		// heatingSetpoint is the windowFunctionTemp, so windowFunction is likely active:
		newWdfValue = 'active'
	}
   
	sendEvent(name: 'optimisation', value: newOptValue)
	sendEvent(name: 'windowFunction', value: newWdfValue)

}


/**
 *  calculateThermostatStatus()
 * 
 *  Calculates thermostatStatus and generates event accordingly.
 *
 *  thermostatStatus is a text summary of thermostatMode and thermostatOperatingState.
 *
 **/
private calculateThermostatStatus() {

	if (state.debug) log.debug "${device.label}: calculateThermostatStatus()"

	def newThermostatStatus = ''
	def thermostatModeDisp = formatThermostatModeForDisp(device.currentValue('thermostatMode'))
	def setpoint = device.currentValue('thermostatSetpoint')
	
	if ('Off' == thermostatModeDisp) {
		newThermostatStatus = 'Off'
	}
	else if('heating' == device.currentValue('thermostatOperatingState')) {
		newThermostatStatus = "Heating to ${setpoint} (${thermostatModeDisp})"
	}
	else {
		newThermostatStatus = "Idle (${thermostatModeDisp})"
	}
	
	sendEvent(name: 'thermostatStatus', value: newThermostatStatus)
}



/**
 *  calculateThermostatSetpointStatus()
 * 
 *  Calculates thermostatSetpointStatus and generates event accordingly.
 *
 *  thermostatSetpointStatus is a text summary of thermostatSetpointMode and thermostatSetpointUntil. 
 *  It also indicates if 'optimisation' or 'windowFunction' is active.
 *
 **/
private calculateThermostatSetpointStatus() {

	if (state.debug) log.debug "${device.label}: calculateThermostatSetpointStatus()"

	def newThermostatSetpointStatus = ''
	def setpointMode = device.currentValue('thermostatSetpointMode')
	
	if ('off' == device.currentValue('thermostatMode')) {
		newThermostatSetpointStatus = 'Off'
	}
	else if ('away' == device.currentValue('thermostatMode')) {
		newThermostatSetpointStatus = 'Away'
	}
	else if ('active' == device.currentValue('optimisation')) {
		newThermostatSetpointStatus = 'Optimisation Active'
	}
	else if ('active' == device.currentValue('windowFunction')) {
		newThermostatSetpointStatus = 'Window Function Active'
	}
	else if ('followSchedule' == setpointMode) {
		newThermostatSetpointStatus = 'Following Schedule'
	}
	else if ('permanentOverride' == setpointMode) {
		newThermostatSetpointStatus = 'Permanent'
	}
	else {
		def untilStr = device.currentValue('thermostatSetpointUntil')
		if (untilStr) {
		
			//def nowDate = new Date()
			
			// thermostatSetpointUntil is an ISO-8601 date format in UTC, and parse() seems to assume date is in UTC.
			def untilDate = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXX", untilStr) 
			def untilDisp = ''
			
			if (untilDate.format("u") == new Date().format("u")) { // Compare day of week to current day of week (today).
				untilDisp = untilDate.format("HH:mm", location.timeZone) // Same day, so just show time.
			}
			else {
				untilDisp = untilDate.format("HH:mm 'on' EEEE", location.timeZone) // Different day, so include name of day.
			}
			newThermostatSetpointStatus = "Temporary Until ${untilDisp}"
		}
		else {
			newThermostatSetpointStatus = "Temporary"
		}
	}
	
	sendEvent(name: 'thermostatSetpointStatus', value: newThermostatSetpointStatus)
}

================================================
FILE: devices/fibaro-dimmer-2/README.md
================================================
# Fibaro Dimmer 2 (FGD-212)
https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2

Copyright (c) [David Lomas](https://github.com/codersaur)

## Overview
<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-on.png" width="200" align="right">
An advanced SmartThings device handler for the Fibaro Dimmer 2 (FGD-212) Z-Wave Dimmer.

### Key features:
* Z-Wave parameters can be configured using the SmartThings GUI.
* Multi-channel device associations can be configured using the SmartThings GUI.
* Child protection modes can be configured using the SmartThings GUI.
* _Fault_ tile indicates burnt-out bulb / overload / hardware errors.
* _Scene_ tile indicates last activated scene.
* _Sync_ tile indicates when all configuration options are successfully synchronised with the physical device.
* Dimmer _level_ range is now 0-100% (instead of 0-99%).
* _Nightmode_ feature allows switch-on brightness level to be controlled on a schedule.
* Logger functionality enables critical errors and warnings to be saved to the _logMessage_ attribute.
* Extensive inline code comments to support community development.

## Installation

1. Follow [these instructions](https://github.com/codersaur/SmartThings#device-handler-installation-procedure) to install the device handler in the SmartThings IDE.

2. **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).

3. 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.
**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!**

## Settings

#### General Settings:
<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-general.png" width="200" align="right">

* **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_.

* **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_.

* **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.

* **Proactively Request Reports**: If you find that device status is slow to update, enabling this setting will cause additional reports to be requested.


#### Child Protection Mode:
<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-protection.png" width="200" align="right">

The 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.

* **Local Protection**: Setting this option to _No operation possible_ will disable both physical switches (S1/S2).

* **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.


#### Nightmode:
<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-nightmode.png" width="200" align="right">

The _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.

* **Nightmode Level**: The dimmer will always switch on at this level when _Nightmode_ is enabled.

* **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.

* **Nightmode Start Time**: _Nightmode_ will be enabled every day at this time.

* **Nightmode Stop Time**: _Nightmode_ will be disabled every day at this time.

If _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.

#### Device Parameters:
<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-params.png" width="200" align="right">

The 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.

If 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.

##### Auto-calibration:
If 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).

Next 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.

##### Read-only Parameters:
The 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.

#### Multi-channel Device Associations:
<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-settings-assocgroups.png" width="200" align="right">

The 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.

The Fibaro Dimmer 2 supports four association groups:

- **Association Group #2**: Sends on/off commands (BASIC_SET) when Switch #1 (S1) is used.
- **Association Group #3**: Sends dim/brighten commands (SWITCH_MUTLILEVEL_SET) when Switch #1 (S1) is used.
- **Association Group #4**: Sends on/off commands (BASIC_SET) when Switch #2 (S2) is used.
- **Association Group #5**: Sends dim/brighten commands (SWITCH_MUTLILEVEL_SET) when Switch #2 (S2) is used.

The 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:
- _Node_: A single hexadecimal number (e.g. "0C") representing the target _Device Network ID_.
- _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.

You 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.

## GUI

#### Power and Energy Tiles:
<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-power-energy.png" width="200" align="right">

These 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.


#### Nightmode Tile:
This tile can be used to toggle (enable/disable) _Nightmode_.

#### Scene Tile:
If 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.

#### Sync Tile:
This 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.

<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-sync.png" width="200">

#### Fault Tile:
This 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.

<img src="https://raw.githubusercontent.com/codersaur/SmartThings/master/devices/fibaro-dimmer-2/screenshots/fd2-ss-tiles-fault.png" width="200">

## SmartApp Integration

#### Attributes:

The device handler has the following attributes:

* **switch [ENUM]**: The switch State, 'On' or 'Off'.
* **level [NUMBER]**: The current light level (0-100%).
* **power [NUMBER]**: The current instantaneous power usage (Watts).
* **energy [NUMBER]**: The Accumulated energy consumption (KWh).
* **energyLastReset**: Last time that the _Accumulated Energy_ figure was reset.
* **scene [NUMBER]**: ID of last-activated scene.
* **nightmode [ENUM]**: Indicates if _Nightmode_ is 'Enabled' or 'Disabled'.
* **syncPending [NUMBER]**: The number of configuration items that need to be synced with the physical device. _0_ if the device is fully synchronised.
* **fault [ENUM]**: Indicates if the device has any faults. '_clear_' if there are no active faults.
* **logMessage [STRING]**: Important log messages.

#### Commands:

The device exposes the following custom commands which can be called from a SmartApp:

* **enableNightmode(level)**: Enable _Nightmode_. The optional level parameter will override the _Nightmode Level_.
* **disableNightmode()**: Disable _Nightmode_.
* **toggleNightmode()**: Toggle _Nightmode_.
* **clearFault()**: Clear any active faults.
* **reset()**: Alias for _resetEnergy()_.
* **resetEnergy()**: Reset the _Accumulated Energy_ figure back to _0_.
* **sync()**: Trigger device synchronisation.

## Version History

#### 2017-02-27: v2.02:
 * Fixed backgroundColor for fault tile.

#### 2017-02-25: v2.01:
  * Preferences: defaultValues are commented out by default to cater for Android users. iPhone users can uncomment these lines if they wish (search for "iPhone").
  * updated(): Fix to allow device to sync after a forced auto-calibration.
  * updateSyncPending(): If a target value is null, then it does not need syncing.

#### 2017-02-24: v2.00
 * Complete re-write in-line with new coding standards.
 * General Behaviour Changes:
  *     Dimmer level now reverts to zero when switched off.
  *     Dimmer level range is now 0-100%.
  *     Fewer report requests are made, as the Fibaro Dimmer 2 is good at sending back reports anyway.
  *     Nightmode scheduling fixed after change in the behaviour of _schedule()_.
 *   Capabilities:
  *     Added "Light" capability.
  *     Added unofficial "Fault" capability. [attributes: 'fault', commands: clearFault()]
  *     Added unofficial "Logging" capability. [attributes: 'logMessage']
  *     Added unofficial "Scene Controller" capability. [attributes: 'scene']
  *     Removed "Configuration" capability and configure() command, as not used.
 *   Attributes:
  *     energyLastReset: renamed from lastReset.
  *     logMessage: Critical error and warning log messages.
  *     syncPending: Number of items that need to be synced with the physical device.
  *     fault: Indicates if the device has any faults (load, surge, overload, overCurrent, voltage, temperature, hardware, firmware). 'clear' if no active faults.
 *   Commands:
  *     resetEnergy(): Resets accumulated energy figure.
  *     clearFault(): Clears any active faults.
 *   Fingerprints: Updated to use new Z-Wave fingerprint format.
 *   Tiles:
  *     level: range is now 0-100%
  *     scene: Indicates last activated scene.
  *     syncPending: Shows when device configuration is synced.
  *     fault: Indicates device faults.
 *   Settings/Preferences:
  *     Proactive Requests.
  *     IDE Live Logging Level
  *     Device Logging Level
  *     Association Group members can be configured from the Settings GUI, including multi-channel endpoint destinations.
  *     Protection Options can be set for local (physical switches) and RF Control, to prevent unintentional changes.
 *   zwaveEvent():
  *     zwaveEvent(CONFIGURATION_REPORT): Uses new scaledConfigurationValue attribute.
  *     zwaveEvent(POWERLEVEL_REPORT): New handler for powerlevel reports.
  *     zwaveEvent(COMMAND_CLASS_SWITCH_BINARY): Removed as it doesn't appear to be supported by the device.
  *     zwaveEvent(ASSOCIATION_REPORT): New handlers for both normal and multi-channel association reports.
  *   dimmerEvent(): Various optimisations and fixes.
 *   update():
  *     Added a check to prevent double execution.
  *     Requests Firmware Metadata, Manufacturer-specific, and Version reports.
 *   New custom commands:
  *     clearFault(): Clears any active fault.
  *     resetEnergy(): Reset the Accumulated Energy figure in the device.
 *   New private helper functions:
  *     logger(): Wrapper function for all logging: Logs events to IDE Live Logging, and also by raising logMessage events. Configured using configLoggingLevelIDE and configLoggingLevelDevice preferences.
  *     sync(): Manages synchronisation of all parameters and association groups with the physical device. The syncPending attribute advertises remaining number of sync operations.
  *     refreshConfig(): Requests all configuration, association group reports.
  *     sendSecureSequence(): Secure an array of commands and send them using sendHubCommand.
  *     Additional functions to dynamically build the parameters and association groups preferences.
 *   New Metadata Funtions:
  *     getCommandClassVersions(): Returns supported command class versions.
  *     getParamsMd(): Returns device parameters metadata (including read-only parameters).
  *     getAssocGroupsMd(): Returns association groups metadata.

#### 2016-10-31: v1.03
  *  Added event handlers for Crc16Encap, SensorMultilevelReport, ManufacturerSpecificReport, VersionReport, and FirmwareMdReport.

#### 2016-10-24: v1.02
  *  Increased delay between ConfigurationSet commands to 500ms to improve reliability of sending parameters.

#### 2016-10-11: v1.01
  *   Added Nightmode functionality.
  *   dimmerEvents(): Fixed MeterGet requests after a switch or level state change.
  *   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).
  *   zwaveEvent(_MeterReport_): Removed rounding of power values. Also, Energy and power values are stored as dispEnergy and dispPower to work-around UI formatting issue.
  *   Settings/Preferences: Fixed param24/25/27 to allow combination of options.
  *   Simplified fingerprint.

#### 2016-10-05: v1.00
  *  Initial version based on device handler by hajar97.
  *  Tiles: Added GetConfig button to retrieve the current device settings (which are displayed in the debug log).

## To Do
 *   Optimise zwaveEvent(CRC_16_ENCAP) by using _ecapsulatedCommand()_, once implemenation has been fixed by SmartThings.
 *   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.
 *   Add _Button_ capability and raise _button_ events.

## Physical Device Notes

General notes relating to the Fibaro Dimmer 2:

* 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.

## References
 Some useful links relevant to the development of this device handler:
* [Fibaro Dimmer 2 - Z-Wave certification information](http://products.z-wavealliance.org/products/1729)
* [Fibaro Dimmer 2 - Manual](http://manuals.fibaro.com/dimmer-2/)

## License

Licensed 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:

    http://www.apache.org/licenses/LICENSE-2.0

Unless 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.


================================================
FILE: devices/fibaro-dimmer-2/fibaro-dimmer-2.groovy
================================================
/*****************************************************************************************************************
 *  Copyright: David Lomas (codersaur)
 *
 *  Name: Fibaro Dimmer 2
 *
 *  Author: David Lomas (codersaur)
 *
 *  Date: 2017-02-27
 *
 *  Version: 2.02
 *
 *  Source: https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2
 *
 *  Author: David Lomas (codersaur)
 *
 *  Description: An advanced SmartThings device handler for the Fibaro Dimmer 2 (FGD-212) Z-Wave Dimmer.
 *
 *  For full information, including installation instructions, exmples, and version history, see:
 *   https://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2
 *
 *  License:
 *   Licensed 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:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless 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.
 *
 *****************************************************************************************************************/
metadata {
    definition (name: "Fibaro Dimmer 2", namespace: "codersaur", author: "David Lomas") {
        capability "Actuator"
        capability "Switch"
        capability "Switch Level"
        capability "Light"
        capability "Sensor"
        capability "Power Meter"
        capability "Energy Meter"
        capability "Polling"
        capability "Refresh"

        // Custom (Virtual) Capabilities:
        //capability "Fault"
        //capability "Logging"
        //capability "Scene Controller"

        // Standard (Capability) Attributes:
        attribute "switch", "string"
        attribute "level", "number"
        attribute "power", "number"
        attribute "energy", "number"

        // Custom Attributes:
        attribute "fault", "string"             // Indicates if the device has any faults. 'clear' if no active faults.
        attribute "logMessage", "string"        // Important log messages.
        attribute "energyLastReset", "string"   // Last time that Accumulated Engergy was reset.
        attribute "syncPending", "number"       // Number of config items that need to be synced with the physical device.
        attribute "nightmode", "string"         // 'Enabled' or 'Disabled'.
        attribute "scene", "number"             // ID of last-activated scene.

        // Display Attributes:
        // These are only required because the UI lacks number formatting and strips leading zeros.
        attribute "dispPower", "string"
        attribute "dispEnergy", "string"

        // Custom Commands:
        command "reset"
        command "resetEnergy"
        command "enableNightmode"
        command "disableNightmode"
        command "toggleNightmode"
        command "clearFault"
        command "sync"
        command "test"

        // Fingerprints (new format):
        fingerprint mfr: "010F", prod: "0102", model: "1000"
        fingerprint type: "1101", mfr: "010F", cc: "5E,86,72,59,73,22,31,32,71,56,98,7A"
        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"
    }

    tiles(scale: 2) {

        // Multi Tile:
        multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
            tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
                attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
                attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
                attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
                attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
            }
            tileAttribute ("device.level", key: "SLIDER_CONTROL", range:"(0..100)") {
                attributeState "level", action:"setLevel"
            }
        }

        // Instantaneous Power:
        valueTile("instMode", "device.dispPower", decoration: "flat", width: 2, height: 1) {
            state "default", label:'Now:', action:"refresh", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_refresh.png"
        }
        valueTile("power", "device.dispPower", decoration: "flat", width: 2, height: 1) {
            state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
        }

        // Accumulated Energy:
        valueTile("energyLastReset", "device.energyLastReset", decoration: "flat", width: 2, height: 1) {
            state "default", label:'Since:  ${currentValue}', action:"resetEnergy", icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_stopwatch_reset.png"
        }
        valueTile("energy", "device.dispEnergy", width: 2, height: 1) {
            state "default", label:'${currentValue}', icon: "https://raw.githubusercontent.com/codersaur/SmartThings/master/icons/tile_2x1_top_bottom_2.png"
        }

        // Other Tiles:
        standardTile("nightmode", "device.nightmode", decoration: "flat", width: 2, height: 2) {
            state "default", label:'${currentValue}', action:"toggleNightmode", icon:"st.Weather.weather4"
        }
        valueTile("scene", "device.scene", decoration: "flat", width: 2, height: 2) {
            state "default", label:'Scene: ${currentValue}'
        }
        standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", label:'', action:"refresh", icon:"st.secondary.refresh"
        }
        standardTile("syncPending", "device.syncPending", decoration: "flat", width: 2, height: 2) {
            state "default", label:'Sync Pending', action:"sync", backgroundColor:"#FF6600"
            state "0", label:'Synced', action:"", backgroundColor:"#79b821"
        }
        standardTile("fault", "device.fault", decoration: "flat", width: 2, height: 2) {
            state "default", label:'${currentValue} Fault', action:"clearFault", backgroundColor:"#FF6600", icon:"st.secondary.tools"
            state "clear", label:'${currentValue}', action:"", backgroundColor:"#79b821", icon:""
        }
        standardTile("test", "device.power", decoration: "flat", width: 2, height: 2) {
            state "default", label:'Test', action:"test"
        }

        // Tile Layouts:
        main(["switch"])
        details([
            "switch",
            "instMode","power",
            "nightmode",
            "energyLastReset","energy",
            "scene",
            //"refresh",
            //"test",
            "syncPending",
            "fault"
        ])
    }

    preferences {

        section { // GENERAL:
            input (
                type: "paragraph",
                element: "paragraph",
                title: "GENERAL:",
                description: "General device handler settings."
            )

            input (
                name: "configLoggingLevelIDE",
                title: "IDE Live Logging Level: Messages with this level and higher will be logged to the IDE.",
                type: "enum",
                options: [
                    "0" : "None",
                    "1" : "Error",
                    "2" : "Warning",
                    "3" : "Info",
                    "4" : "Debug",
                    "5" : "Trace"
                ],
//                defaultValue: "3", // iPhone users can uncomment these lines!
                required: true
            )

            input (
                name: "configLoggingLevelDevice",
                title: "Device Logging Level: Messages with this level and higher will be logged to the logMessage attribute.",
                type: "enum",
                options: [
                    "0" : "None",
                    "1" : "Error",
                    "2" : "Warning"
                ],
//                defaultValue: "2", // iPhone users can uncomment these lines!
                required: true
            )

            input (
                name: "configSyncAll",
                title: "Force Full Sync: All device parameters, association groups, and protection settings will " +
                "be re-sent to the device. This will take several minutes and you may need to press the 'sync' " +
                "tile a few times.",
                type: "boolean",
//                defaultValue: false, // iPhone users can uncomment these lines!
                required: true
            )

            input (
                name: "configProactiveReports",
                title: "Proactively Request Reports: Additonal requests for status reports will be made. " +
                "Use only if status reporting is unreliable.",
                type: "boolean",
//                defaultValue: false, // iPhone users can uncomment these lines!
                required: true
            )
        }

        section { // PROTECTION:
            input type: "paragraph",
                element: "paragraph",
                title: "PROTECTION:",
                description: "Prevent unintentional control (e.g. by a child) by disabling the physical switches and/or RF control."

            input (
                name: "configProtectLocal",
                title: "Local Protection: Applies to physical switches:",
                type: "enum",
                options: [
                    "0" : "Unprotected",
                    //"1" : "Protection by sequence", // Not supported by Fibaro Dimmer 2.
                    "2" : "No operation possible"
                ],
//                defaultValue: "0", // iPhone users can uncomment these lines!
                required: true
            )

            input (
                name: "configProtectRF",
                title: "RF Protection: Applies to Z-Wave commands sent from hub or other devices:",
                type: "enum",
                options: [
                    "0" : "Unprotected",
                    "1" : "No RF control"//,
                    //"2" : "No RF response" // Not supported by Fibaro Dimmer 2.
                ],
//                defaultValue: "0", // iPhone users can uncomment these lines!
                required: true
            )

        }

        section { // NIGHTMODE:
            input type: "paragraph",
                element: "paragraph",
                title: "NIGHTMODE:",
                description: "Nightmode forces the dimmer to switch on at a specific level (e.g. low-level during the night).\n" +
                    "Nightmode can be enabled/disabled manually using the new Nightmode tile, or scheduled below."

            input type: "number",
                name: "configNightmodeLevel",
                title: "Nightmode Level: The dimmer will always switch on at this level when nightmode is enabled.",
                range: "1..100",
//                defaultValue: "10", // iPhone users can uncomment these lines!
                required: true

            input type: "boolean",
                name: "configNightmodeForce",
                title: "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).",
//                defaultValue: true, // iPhone users can uncomment these lines!
                required: true

            input type: "time",
                name: "configNightmodeStartTime",
                title: "Nightmode Start Time: Nightmode will be enabled every day at this time.",
                required: false

            input type: "time",
                name: "configNightmodeStopTime",
                title: "Nightmode Stop Time: Nightmode will be disabled every day at this time.",
                required: false
        }

        generatePrefsParams()

        generatePrefsAssocGroups()

    }
}

/**
 *  parse()
 *
 *  Called when messages from the device are received by the hub. The parse method is responsible for interpreting
 *  those messages and returning event definitions (and command responses).
 *
 *  As this is a Z-wave device, zwave.parse() is used to convert the message into a command. The command is then
 *  passed to zwaveEvent(), which is overloaded for each type of command below.
 *
 *  Parameters:
 *   String      description        The raw message from the device.
 **/
def parse(description) {
    logger("parse(): Parsing raw message: ${description}","trace")

    def result = null

    if (description.startsWith("Err")) {
        logger("parse(): Unknown Error. Raw message: ${description}","error")
    }
    else if (description != "updated") {
        // The purpose of the replace statement here is to fix a bug, see:
        // https://community.smartthings.com/t/wireless-wall-switch-zme-wallc-s-to-control-smartthings-devices-and-routines/24810/28
        def cmd = zwave.parse(description.replace("98C1", "9881"), getCommandClassVersions())
        if (cmd) {
            result = zwaveEvent(cmd)
        } else {
            logger("parse(): Could not parse raw message: ${description}","error")
        }
    }

    return result
}

/*****************************************************************************************************************
 *  Z-wave Event Handlers.
 *****************************************************************************************************************/

/**
 *  zwaveEvent( COMMAND_CLASS_BASIC V1 (0x20) : BASIC_REPORT )
 *
 *  The Basic Report command is used to advertise the status of the primary functionality of the device.
 *
 *  Action: Pass command to dimmerEvent().
 *
 *  cmd attributes:
 *    Short    value
 *      0x00       = Off
 *      0x01..0x63 = 0..100%
 *      0xFE       = Unknown
 *      0xFF       = On
 **/
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
    logger("zwaveEvent(): Basic Report received: ${cmd}","trace")
    return dimmerEvent(cmd)
}

/**
 *  zwaveEvent( COMMAND_CLASS_BASIC V1 (0x20) : BASIC_SET )
 *
 *  The Basic Set command is used to set a value in a supporting device.
 *  If this command is received by the hub, the hub must be a member of one or more association groups.
 *
 *  Action: No action required as state change will be triggered via BASIC_REPORT handler.
 *
 *  cmd attributes:
 *    Short    value
 *      0x00       = Off
 *      0x01..0x63 = 0..100%
 *      0xFE       = Unknown
 *      0xFF       = On
 *
 *  Example: BasicSet(value: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
    logger("zwaveEvent(): Basic Set received: ${cmd}","trace")
}

/**
 *  zwaveEvent( COMMAND_CLASS_SWITCH_MULTILEVEL V3 (0x26) : SWITCH_MULTILEVEL_REPORT )
 *
 *  The Switch Multilevel Report is used to advertise the status of a multilevel device.
 *
 *  Action: Pass command to dimmerEvent().
 *
 *  cmd attributes:
 *    Short    value
 *      0x00       = Off
 *      0x01..0x63 = 0..100%
 *      0xFE       = Unknown
 *      0xFF       = On [Deprecated]
 *
 *  Example: SwitchMultilevelReport(value: 1)
 **/
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) {
    logger("zwaveEvent(): Switch Multilevel Report received: ${cmd}","trace")
    return dimmerEvent(cmd)
}

/**
 *  zwaveEvent( COMMAND_CLASS_SWITCH_MULTILEVEL V3 (0x26) : SWITCH_MULTILEVEL_SET )
 *
 *  The Switch Multilevel Set command is used to set a value in a supporting device.
 *  If this command is received by the hub, the hub must be a member of one or more association groups.
 *
 *  Action: No action required as state change will be triggered via SWITCH_MULTILEVEL_REPORT handler.
 *
 *  cmd attributes:
 *    Short    value
 *      0x00       = Off
 *      0x01..0x63 = 0..100%
 *      0xFE       = Unknown
 *      0xFF       = On [Deprecated]
 *    Short    dimmingDuration
 *
 *  Example: SwitchMultilevelSet(dimmingDuration: 1, value: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelSet cmd) {
    logger("zwaveEvent(): Switch Multilevel Set received: ${cmd}","trace")
}

/**
 *  zwaveEvent( COMMAND_CLASS_SWITCH_MULTILEVEL V3 (0x26) : SWITCH_MULTILEVEL_START_LEVEL_CHANGE )
 *
 *  The Multilevel Switch Start Level Change command is used to initiate a transition to a new level.
 *  If this command is received by the hub, the hub must be a member of one or more association groups.
 *
 *  Action: No action required as state change will be triggered via a SWITCH_MULTILEVEL_REPORT on completion
 *  of the transition.
 *
 *  cmd attributes:
 *    Short    dimmingDuration
 *    Boolean  ignoreStartLevel
 *    Short    incDec
 *    Short    startLevel
 *    Short    stepSize
 *    Short    upDown
 *
 *  Example: SwitchMultilevelStartLevelChange(dimmingDuration: 3, ignoreStartLevel: false, incDec: 0,
 *            reserved00: 0, startLevel: 4, stepSize: 1, upDown: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd) {
    logger("zwaveEvent():  Switch Multilevel Start Level Change received: ${cmd}","trace")
}

/**
 *  zwaveEvent( COMMAND_CLASS_SWITCH_MULTILEVEL V3 (0x26) : SWITCH_MULTILEVEL_STOP_LEVEL_CHANGE )
 *
 *  The Multilevel Switch Stop Level Change command is used to stop an ongoing transition.
 *  If this command is received by the hub, the hub must be a member of one or more association groups.
 *
 *  Action: No action required as state change will be triggered via a SWITCH_MULTILEVEL_REPORT on completion
 *  of the transition.
 *
 *  cmd attributes: None
 *
 *  Example: SwitchMultilevelStopLevelChange()
 **/
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) {
    logger("zwaveEvent():  Switch Multilevel Stop Level Change received: ${cmd}","trace")
}

/**
 *  dimmerEvent()
 *
 *  Common handler for BasicReport, SwitchBinaryReport, SwitchMultilevelReport.
 *
 *  Action: Raise 'switch' and 'level' events.
 *   Restore pending level if dimmer has been switched on after nightmode has been disabled.
 *   If Proactive Reporting is enabled, and the level has changed, request a meter report.
 **/
def dimmerEvent(physicalgraph.zwave.Command cmd) {

    def result = []

    // switch event:
    def switchValue = (cmd.value ? "on" : "off")
    def switchEvent = createEvent(name: "switch", value: switchValue)
    if (switchEvent.isStateChange) logger("Dimmer turned ${switchValue}.","info")
    result << switchEvent

    // level event:
    def levelValue = Math.round (cmd.value * 100 / 99)
    def levelEvent = createEvent(name: "level", value: levelValue, unit: "%")
    if (levelEvent.isStateChange) logger("Dimmer level is ${levelValue}%","info")
    result << levelEvent

    // Store last active level, which is needed for nightmode functionality:
    if (levelValue > 0) state.lastActiveLevel = levelValue

    // Restore pending level if dimmer has been switched on after nightmode has been disabled:
    if (!state.nightmodeActive & (state.nightmodePendingLevel > 0) & switchEvent.isStateChange & switchValue == "on") {
        logger("dimmerEvent(): Applying Pending Level: ${state.nightmodePendingLevel}","debug")
        result << response(secure(zwave.basicV1.basicSet(value: Math.round(state.nightmodePendingLevel.toInteger() * 99 / 100 ))))
        state.nightmodePendingLevel = 0
    }
    // Else if Proactive Reporting is enabled, and the level has changed, request a meter report:
    else if (state.proactiveReports & levelEvent.isStateChange) {
        result << response(["delay 5000", secure(zwave.meterV3.meterGet(scale: 2)),"delay 10000", secure(zwave.meterV3.meterGet(scale: 2))])
        // Meter request is delayed for 5s, although sometimes this isn't long enough, so make a second request after another 10 seconds.
    }

    return result
}

/**
 *  zwaveEvent( COMMAND_CLASS_SWITCH_ALL V1 (0x27) : SWITCH_ALL_REPORT )
 *
 *  The All Switch Report Command is used to report if the device is included or excluded from the all on/all off
 *  functionality.
 *
 *  Note: The Fibaro Dimmer 2 supports control of this functionality via Parameter #11, in addition to
 *  SWITCH_ALL_SET commands.
 *
 *  Action: Log an info message.
 *
 *  cmd attributes:
 *    Short    mode
 *      0   = MODE_EXCLUDED_FROM_THE_ALL_ON_ALL_OFF_FUNCTIONALITY
 *      1   = MODE_EXCLUDED_FROM_THE_ALL_ON_FUNCTIONALITY_BUT_NOT_ALL_OFF
 *      2   = MODE_EXCLUDED_FROM_THE_ALL_OFF_FUNCTIONALITY_BUT_NOT_ALL_ON
 *      255 = MODE_INCLUDED_IN_THE_ALL_ON_ALL_OFF_FUNCTIONALITY
 **/
def zwaveEvent(physicalgraph.zwave.commands.switchallv1.SwitchAllReport cmd) {
    logger("zwaveEvent(): Switch All Report received: ${cmd}","trace")

    def msg = ""
    switch (cmd.mode) {
            case 0:
                msg = "Device is excluded from the all on/all off functionality."
                break

            case 1:
                msg = "Device is excluded from the all on functionality but not all off."
                break

            case 2:
                msg = "Device is excluded from the all off functionality but not all on."
                break

            default:
                msg = "Device is included in the all on/all off functionality."
                break
    }

    logger("Switch All Mode: ${msg}","info")

    return msg
}

/**
 *  zwaveEvent( COMMAND_CLASS_SCENE_ACTIVATION (0x2B) : SCENE_ACTIVATION_SET )
 *
 *  The Scene Activation Set Command is used to activate the setting associated to the scene ID.
 *
 *  Action: Raise scene event and log an info message.
 *
 *  cmd attributes:
 *    Short    dimmingDuration
 *      0x00       = Instantly
 *      0x01..0x7F = 1 second (0x01) to 127 seconds (0x7F) in 1-second resolution.
 *      0x80..0xFE = 1 minute (0x80) to 127 minutes (0xFE) in 1-minute resolution.
 *      0xFF       = Dimming duration configured by the Scene Actuator Configuration Set and Scene
 *                   Controller Configuration Set Command depending on device used.
 *    Short    sceneId
 *      0x00..0xFF = Scene0..Scene255
 **/
def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) {
    logger("zwaveEvent(): Scene Activation Set received: ${cmd}","trace")

    def result = []
    result << createEvent(name: "scene", value: "$cmd.sceneId", data: [switchType: "$settings.param20"], descriptionText: "Scene id ${cmd.sceneId} was activated", isStateChange: true)

    logger("Scene #${cmd.sceneId} was activated.","info")

    return result
}

/**
 *  zwaveEvent( COMMAND_CLASS_SENSOR_MULTILEVEL V4 (0x31) : SENSOR_MULTILEVEL_REPORT )
 *
 *  The Multilevel Sensor Report Command is used by a multilevel sensor to advertise a sensor reading.
 *
 *  Action: Raise appropriate type of event (and disp event) and log an info message.
 *
 *  Note: SmartThings does not yet have capabilities corresponding to all possible sensor types, therefore
 *  some of the event types raised below are non-standard.
 *
 *  Note: Fibaro Dimmer 2 appears to report power (sensorType 4) only.
 *
 *  cmd attributes:
 *    Short         precision           Indicates the number of decimals.
 *                                      E.g. The decimal value 1025 with precision 2 is therefore equal to 10.25.
 *    Short         scale               Indicates what unit the sensor uses.
 *    BigDecimal    scaledSensorValue   Sensor value as a double.
 *    Short         sensorType          Sensor Type (8 bits).
 *    List<Short>   sensorValue         Sensor value as an array of bytes.
 *    Short         size                Indicates the number of bytes used for the sensor value.
 **/
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv4.SensorMultilevelReport cmd) {
    logger("zwaveEvent(): SensorMultilevelReport received: ${cmd}","trace")

    def result = []
    def map = [ displayed: true, value: cmd.scaledSensorValue.toString() ]
    def dispMap = [ displayed: false ]

    // Sensor Types up to V4 only, there are further sensor types up to V10 defined.
    switch (cmd.sensorType) {
        case 1:  // Air Temperature (V1)
            map.name = "temperature"
            map.unit = (cmd.scale == 1) ? "F" : "C"
            break

        case 2:  // General Purpose (V1)
            map.name = "value"
            map.unit = (cmd.scale == 1) ? "" : "%"
            break

        case 3:  // Luninance (V1)
            map.name = "illuminance"
            map.unit = (cmd.scale == 1) ? "lux" : "%"
            break

        case 4:  // Power (V2)
            map.name = "power"
            map.unit = (cmd.scale == 1) ? "Btu/h" : "W"
            dispMap.name = "dispPower"
            dispMap.value = String.format("%.1f",cmd.scaledSensorValue as BigDecimal) + " ${map.unit}"
            break

        case 5:  // Humidity (V2)
            map.name = "humidity"
            map.unit = (cmd.scale == 1) ? "g/m^3" : "%"
            break

        case 6:  // Velocity (V2)
            map.name = "velocity"
            map.unit = (cmd.scale == 1) ? "mph" : "m/s"
            break

        case 7:  // Direction (V2)
            map.name = "direction"
            map.unit = ""
            break

        case 8:  // Atmospheric Pressure (V2)
        case 9:  // Barometric Pressure (V2)
            map.name = "pressure"
            map.unit = (cmd.scale == 1) ? "inHg" : "kPa"
            break

        case 0xA:  // Solar Radiation (V2)
            map.name = "radiation"
            map.unit = "W/m^3"
            break

        case 0xB:  // Dew Point (V2)
            map.name = "dewPoint"
            map.unit = (cmd.scale == 1) ? "F" : "C"
            break

        case 0xC:  // Rain Rate (V2)
            map.name = "rainRate"
            map.unit = (cmd.scale == 1) ? "in/h" : "mm/h"
            break

        case 0xD:  // Tide Level (V2)
            map.name = "tideLevel"
            map.unit = (cmd.scale == 1) ? "ft" : "m"
            break

        case 0xE:  // Weight (V3)
            map.name = "weight"
            map.unit = (cmd.scale == 1) ? "lbs" : "kg"
            break

        case 0xF:  // Voltage (V3)
            map.name = "voltage"
            map.unit = (cmd.scale == 1) ? "mV" : "V"
            dispMap.name = "dispVoltage"
            dispMap.value = String.format("%.1f",cmd.scaledSensorValue as BigDecimal) + " ${map.unit}"
            break

        case 0x10:  // Current (V3)
            map.name = "current"
            map.unit = (cmd.scale == 1) ? "mA" : "A"
            dispMap.name = "dispCurrent"
            dispMap.value = String.format("%.1f",cmd.scaledSensorValue as BigDecimal) + " ${map.unit}"
            break

        case 0x11:  // Carbon Dioxide Level (V3)
            map.name = "carbonDioxide"
            map.unit = "ppm"
            break

        case 0x12:  // Air Flow (V3)
            map.name = "fluidFlow"
            map.unit = (cmd.scale == 1) ? "cfm" : "m^3/h"
            break

        case 0x13:  // Tank Capacity (V3)
            map.name = "fluidVolume"
            map.unit = (cmd.scale == 0) ? "ltr" : (cmd.scale == 1) ? "m^3" : "gal"
            break

        case 0x14:  // Distance (V3)
            map.name = "distance"
            map.unit = (cmd.scale == 0) ? "m" : (cmd.scale == 1) ? "cm" : "ft"
            break

        default:
            logger("zwaveEvent(): SensorMultilevelReport with unhandled sensorType: ${cmd}","warn")
            map.name = "unknown"
            map.unit = "unknown"
            break
    }

    logger("New sensor reading: Name: ${map.name}, Value: ${map.value}, Unit: ${map.unit}","info")

    result << createEvent(map)
    if (dispMap.name) { result << createEvent(dispMap) }

    return result
}

/**
 *  zwaveEvent( COMMAND_CLASS_METER V3 (0x32) : METER_REPORT )
 *
 *  The Meter Report Command is used to advertise a meter reading.
 *
 *  Action: Raise appropriate type of event (and disp... event) and log an info message.
 *
 *  Note: Fibaro Dimmer 2 supports energy and power only. It will not report current, voltage, or power factor.
 *
 *  cmd attributes:
 *    Integer        deltaTime                   Time in seconds since last report.
 *    Short          meterType                   Specifies the type of metering device.
 *      0x00 = Unknown
 *      0x01 = Electric meter
 *      0x02 = Gas meter
 *      0x03 = Water meter
 *    List<Short>    meterValue                  Meter value as an array of bytes.
 *    Double         scaledMeterValue            Meter value as a double.
 *    List<Short>    previousMeterValue          Previous meter value as an array of bytes.
 *    Double         scaledPreviousMeterValue    Previous meter value as a double.
 *    Short          size                        The size of the array for the meterValue and previousMeterValue.
 *    Short          scale                       Indicates what unit the sensor uses (dependent on meterType).
 *    Short          precision                   The decimal precision of the values.
 *    Short          rateType                    Specifies if it is import or export values to be read.
 *      0x01 = Import (consumed)
 *      0x02 = Export (produced)
 *    Boolean        scale2                      ???
 **/
def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) {
    logger("zwaveEvent(): Meter Report received: ${cmd}","trace")

    def result = []

    switch (cmd.meterType) {
        case 1:  // Electric meter:
            switch (cmd.scale) {
                case 0:  // Accumulated Energy (kWh):
                    result << createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh", displayed: true)
                    result << createEvent(name: "dispEnergy", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal) + " kWh", displayed: false)
                    logger("New meter reading: Accumulated Energy: ${cmd.scaledMeterValue} kWh","info")
                    break

                case 1:  // Accumulated Energy (kVAh):
                    result << createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh", displayed: true)
                    result << createEvent(name: "dispEnergy", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal) + " kVAh", displayed: false)
                    logger("New meter reading: Accumulated Energy: ${cmd.scaledMeterValue} kVAh","info")
                    break

                case 2:  // Instantaneous Power (Watts):
                    result << createEvent(name: "power", value: cmd.scaledMeterValue, unit: "W", displayed: true)
                    result << createEvent(name: "dispPower", value: String.format("%.1f",cmd.scaledMeterValue as BigDecimal) + " W", displayed: false)
                    logger("New meter reading: Instantaneous Power: ${cmd.scaledMeterValue} W","info")
                    break

                case 3:  // Accumulated Pulse Count:
                    result << createEvent(name: "pulseCount", value: cmd.scaledMeterValue, unit: "", displayed: true)
                    logger("New meter reading: Accumulated Electricity Pulse Count: ${cmd.scaledMeterValue}","info")
                    break

                case 4:  // Instantaneous Voltage (Volts):
                    result << createEvent(name: "voltage", value: cmd.scaledMeterValue, unit: "V", displayed: true)
                    result << createEvent(name: "dispVoltage", value: String.format("%.1f",cmd.scaledMeterValue as BigDecimal) + " V", displayed: false)
                    logger("New meter reading: Instantaneous Voltage: ${cmd.scaledMeterValue} V","info")
                    break

                 case 5:  // Instantaneous Current (Amps):
                    result << createEvent(name: "current", value: cmd.scaledMeterValue, unit: "A", displayed: true)
                    result << createEvent(name: "dispCurrent", value: String.format("%.1f",cmd.scaledMeterValue as BigDecimal) + " V", displayed: false)
                    logger("New meter reading: Instantaneous Current: ${cmd.scaledMeterValue} A","info")
                    break

                 case 6:  // Instantaneous Power Factor:
                    result << createEvent(name: "powerFactor", value: cmd.scaledMeterValue, unit: "", displayed: true)
                    result << createEvent(name: "dispPowerFactor", value: String.format("%.1f",cmd.scaledMeterValue as BigDecimal), displayed: false)
                    logger("New meter reading: Instantaneous Power Factor: ${cmd.scaledMeterValue}","info")
                    break

                default:
                    logger("zwaveEvent(): Meter Report with unhandled scale: ${cmd}","warn")
                    break
            }
            break

        case 2:  // Gas meter:

            switch (cmd.scale) {
                case 0:  // Accumulated Gas Volume (m^3):
                    result << createEvent(name: "fluidVolume", value: cmd.scaledMeterValue, unit: "m^3", displayed: true)
                    result << createEvent(name: "dispFluidVolume", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal) + " m^3", displayed: false)
                    logger("New meter reading: Accumulated Gas Volume: ${cmd.scaledMeterValue} m^3","info")
                    break

                case 1:  // Accumulated Gas Volume (ft^3):
                    result << createEvent(name: "fluidVolume", value: cmd.scaledMeterValue, unit: "ft^3", displayed: true)
                    result << createEvent(name: "dispFluidVolume", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal) + " ft^3", displayed: false)
                    logger("New meter reading: Accumulated Gas Volume: ${cmd.scaledMeterValue} ft^3","info")
                    break

                case 3:  // Accumulated Pulse Count:
                    result << createEvent(name: "pulseCount", value: cmd.scaledMeterValue, unit: "", displayed: true)
                    logger("New meter reading: Accumulated Gas Pulse Count: ${cmd.scaledMeterValue}","info")
                    break

                default:
                    logger("zwaveEvent(): Meter Report with unhandled scale: ${cmd}","warn")
                    break
            }
            break

        case 3:  // Water meter:

            switch (cmd.scale) {
                case 0:  // Accumulated Water Volume (m^3):
                    result << createEvent(name: "fluidVolume", value: cmd.scaledMeterValue, unit: "m^3", displayed: true)
                    result << createEvent(name: "dispFluidVolume", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal) + " m^3", displayed: false)
                    logger("New meter reading: Accumulated Water Volume: ${cmd.scaledMeterValue} m^3","info")
                    break

                case 1:  // Accumulated Water Volume (ft^3):
                    result << createEvent(name: "fluidVolume", value: cmd.scaledMeterValue, unit: "ft^3", displayed: true)
                    result << createEvent(name: "dispFluidVolume", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal) + " ft^3", displayed: false)
                    logger("New meter reading: Accumulated Water Volume: ${cmd.scaledMeterValue} ft^3","info")
                    break

                case 2:  // Accumulated Water Volume (US gallons):
                    result << createEvent(name: "fluidVolume", value: cmd.scaledMeterValue, unit: "gal", displayed: true)
                    result << createEvent(name: "dispFluidVolume", value: String.format("%.2f",cmd.scaledMeterValue as BigDecimal) + " gal", displayed: false)
                    logger("New meter reading: Accumulated Water Volume: ${cmd.scaledMeterValue} gal","info")
                    break

                case 3:  // Accumulated Pulse Count:
                    result << createEvent(name: "pulseCount", value: cmd.scaledMeterValue, unit: "", displayed: true)
                    logger("New meter reading: Accumulated Water Pulse Count: ${cmd.scaledMeterValue}","info")
                    break

                default:
                    logger("zwaveEvent(): Meter Report with unhandled scale: ${cmd}","warn")
                    break
            }
            break

        default:
            logger("zwaveEvent(): Meter Report with unhandled meterType: ${cmd}","warn")
            break
    }

    return result
}

/**
 *  zwaveEvent( COMMAND_CLASS_CRC16_ENCAP V1 (0x56) : CRC_16_ENCAP )
 *
 *  The CRC-16 Encapsulation Command Class is used to encapsulate a command with an additional CRC-16 checksum
 *  to ensure integrity of the payload. The purpose for this command class is to ensure a higher integrity level
 *  of payloads carrying important data.
 *
 *  Action: Extract the encapsulated command and pass to zwaveEvent().
 *
 *  Note: Validation of the checksum is not necessary as this is performed by the hub.
 *
 *  cmd attributes:
 *    Integer      checksum      Checksum.
 *    Short        command       Command identifier of the embedded command.
 *    Short        commandClass  Command Class identifier of the embedded command.
 *    List<Short>  data          Embedded command data.
 *
 *  Example: Crc16Encap(checksum: 125, command: 2, commandClass: 50, data: [33, 68, 0, 0, 0, 194, 0, 0, 77])
 **/
def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) {
    logger("zwaveEvent(): CRC-16 Encapsulation Command received: ${cmd}","trace")

    def versions = getCommandClassVersions()
    def version = versions[cmd.commandClass as Integer]
    def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass)
    def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data)
    // TO DO: It should be possible to replace the lines above with this line soon...
    //def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())
    if (!encapsulatedCommand) {
        logger("zwaveEvent(): Could not extract command from ${cmd}","error")
    } else {
        return zwaveEvent(encapsulatedCommand)
    }
}

/**
 *  zwaveEvent( COMMAND_CLASS_DEVICE_RESET_LOCALLY V1 (0x5A) : DEVICE_RESET_LOCALLY_NOTIFICATION )
 *
 *  The Device Reset Locally Notification Command is used to advertise that the device will be reset.
 *
 *  Action: Log a warn message.
 **/
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
    logger("zwaveEvent(): Device Reset Locally Notification: ${cmd}","trace")
    logger("zwaveEvent(): Device was reset!","warn")
}

/**
 *  zwaveEvent( COMMAND_CLASS_MULTICHANNEL V4 (0x60) : MULTI_CHANNEL_CMD_ENCAP )
 *
 *  The Multi Channel Command Encapsulation command is used to encapsulate commands. Any command supported by
 *  a Multi Channel End Point may be encapsulated using this command.
 *
 *  Action: Extract the encapsulated command and pass to the appropriate zwaveEvent() handler.
 *
 *  Note: We only receive these commands from a Dimmer 2 if the hub has been added to one or more association
 *  groups 2-5, which is not normally needed. The sourceEndPoint attribute will indicate if from S1 or S2, but we
 *  don't care here, because button presses are handled via SCENE_ACTIVATION_SET commands instead.
 *
 *  cmd attributes:
 *    Boolean      bitAddress           Set to true if multicast addressing is used.
 *    Short        command              Command identifier of the embedded command.
 *    Short        commandClass         Command Class identifier of the embedded command.
 *    Short        destinationEndPoint  Destination End Point.
 *    List<Short>  parameter            Carries the parameter(s) of the embedded command.
 *    Short        sourceEndPoint       Source End Point.
 *
 *  Example: MultiChannelCmdEncap(bitAddress: false, command: 1, commandClass: 32, destinationEndPoint: 0,
 *            parameter: [0], sourceEndPoint: 1)
 **/
def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
    logger("zwaveEvent(): Multi Channel Command Encapsulation command received: ${cmd}","trace")

    def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())
    if (!encapsulatedCommand) {
        logger("zwaveEvent(): Could not extract command from ${cmd}","error")
    } else {
        return zwaveEvent(encapsulatedCommand)
    }
}

/**
 *  zwaveEvent( COMMAND_CLASS_CONFIGURATION V1 (0x70) : CONFIGURATION_REPORT )
 *
 *  The Configuration Report Command is used to advertise the actual value of the advertised parameter.
 *
 *  Action: Store the value in the parameter cache, update syncPending, and log an info message.
 *
 *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this
 *  is not possible due to security restrictions in the SmartThings platform.
 *
 *  cmd attributes:
 *    List<Short>  configurationValue  Value of parameter (byte array).
 *    Short        parameterNumber     Parameter ID.
 *    Short        size                Size of parameter's value (bytes).
 *
 *  Example: ConfigurationReport(configurationValue: [0], parameterNumber: 14, reserved11: 0,
 *            scaledConfigurationValue: 0, size: 1)
 **/
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
    logger("zwaveEvent(): Configuration Report received: ${cmd}","trace")

    state."paramCache${cmd.parameterNumber}" = cmd.scaledConfigurationValue.toInteger()
    def paramName = getParamsMd().find( { it.id == cmd.parameterNumber }).name
    logger("Parameter #${cmd.parameterNumber} [${paramName}] has value: ${cmd.scaledConfigurationValue}","info")
    updateSyncPending()
}

/**
 *  zwaveEvent( COMMAND_CLASS_NOTIFICATION V3 (0x71) : NOTIFICATION_REPORT )
 *
 *  The Notification Report Command is used to advertise notification information.
 *
 *  Action: Raise appropriate type of event (e.g. fault, tamper, water) and log an info or warn message.
 *
 *  Note: SmartThings does not yet have official capabilities definited for many types of notification. E.g. this
 *  handler raises 'fault' events, which is not part of any standard capability.
 *
 *  cmd attributes:
 *    Short        event                  Event Type (see code below).
 *    List<Short>  eventParameter         Event Parameter(s) (depends on Event type).
 *    Short        eventParametersLength  Length of eventParameter.
 *    Short        notificationStatus     The notification reporting status of the device (depends on push or pull model).
 *    Short        notificationType       Notification Type (see code below).
 *    Boolean      sequence
 *    Short        v1AlarmLevel           Legacy Alarm Level from Alarm CC V1.
 *    Short        v1AlarmType            Legacy Alarm Type from Alarm CC V1.
 *    Short        zensorNetSourceNodeId  Source node ID
 *
 *  Example: NotificationReport(event: 8, eventParameter: [], eventParametersLength: 0, notificationStatus: 255,
 *    notificationType: 8, reserved61: 0, sequence: false, v1AlarmLevel: 0, v1AlarmType: 0, zensorNetSourceNodeId: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
    logger("zwaveEvent(): Notification Report received: ${cmd}","trace")

    def result = []

    switch (cmd.notificationType) {
        //case 1:  // Smoke Alarm: // Not Implemented yet. Should raise smoke/carbonMonoxide/consumableStatus events etc...
        //case 2:  // CO Alarm:
        //case 3:  // CO2 Alarm:

        case 4:  // Heat Alarm:
            switch (cmd.event) {
                case 0:  // Previous Events cleared:
                    // Do not send a fault clear event automatically.
                    logger("Heat Alarm Cleared","info")
                    break

                case 1:  // Overheat detected:
                case 2:  // Overheat detected, Unknown Location:
                    result << createEvent(name: "fault", value: "overheat", descriptionText: "Overheat detected!", displayed: true)
                    logger("Overheat detected!","warn")
                    break

                case 3:  // Rapid Temperature Rise:
                case 4:  // Rapid Temperature Rise, Unknown Location:
                    result << createEvent(name: "fault", value: "temperature", descriptionText: "Rapid temperature rise detected!", displayed: true)
                    logger("Rapid temperature rise detected!","warn")
                    break

                case 5:  // Underheat detected:
                case 6:  // Underheat detected, Unknown Location:
                    result << createEvent(name: "fault", value: "underheat", descriptionText: "Underheat detected!", displayed: true)
                    logger("Underheat detected!","warn")
                    break

                default:
                    logger("zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}","warn")
                    break
            }
            break

        //case 5:  // Water Alarm: // Not Implemented yet. Should raise water/consumableStatus events etc...

        case 8:  // Power Management:
            switch (cmd.event) {
                case 0:  // Previous Events cleared:
                    // Do not send a fault clear event automatically.
                    logger("Previous Events cleared","info")
                    break

                //case 1:  // Mains Connected:
                //case 2:  // AC Mains Disconnected:
                //case 3:  // AC Mains Re-connected:

                case 4:  // Surge:
                    result << createEvent(name: "fault", value: "surge", descriptionText: "Power surge detected!", displayed: true)
                    logger("Power surge detected!","warn")
                    break

                case 5:  // Voltage Drop:
                    result << createEvent(name: "fault", value: "voltage", descriptionText: "Voltage drop detected!", displayed: true)
                    logger("Voltage drop detected!","warn")
                    break

                case 6:  // Over-current:
                    result << createEvent(name: "fault", value: "current", descriptionText: "Over-current detected!", displayed: true)
                    logger("Over-current detected!","warn")
                    break

                 case 7:  // Over-Voltage:
                    result << createEvent(name: "fault", value: "voltage", descriptionText: "Over-voltage detected!", displayed: true)
                    logger("Over-voltage detected!","warn")
                    break

                 case 8:  // Overload:
                    result << createEvent(name: "fault", value: "load", descriptionText: "Overload detected!", displayed: true)
                    logger("Overload detected!","warn")
                    break

                 case 9:  // Load Error:
                    result << createEvent(name: "fault", value: "load", descriptionText: "Load Error detected!", displayed: true)
                    logger("Load Error detected!","warn")
                    break

                default:
                    logger("zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}","warn")
                    break
            }
            break

        case 9:  // system:
            switch (cmd.event) {
                case 0:  // Previous Events cleared:
                    // Do not send a fault clear event automatically.
                    logger("Previous Events cleared","info")
                    break

                case 1:  // Harware Failure:
                case 3:  // Harware Failure (with manufacturer proprietary failure code):
                    result << createEvent(name: "fault", value: "hardware", descriptionText: "Hardware failure detected!", displayed: true)
                    logger("Hardware failure detected!","warn")
                    break

                case 2:  // Software Failure:
                case 4:  // Software Failure (with manufacturer proprietary failure code):
                    result << createEvent(name: "fault", value: "firmware", descriptionText: "Firmware failure detected!", displayed: true)
                    logger("Firmware failure detected!","warn")
                    break

                case 6:  // Tampering:
                    result << createEvent(name: "tamper", value: "detected", descriptionText: "Tampering: Product covering removed!", displayed: true)
                    logger("Tampering: Product covering removed!","warn")
                    break

                default:
                    logger("zwaveEvent(): Notification Report recieved with unhandled event: ${cmd}","warn")
                    break
            }
            break

        default:
            logger("zwaveEvent(): Notification Report recieved with unhandled notificationType: ${cmd}","warn")
            break
    }

    return result
}

/**
 *  zwaveEvent( COMMAND_CLASS_MANUFACTURER_SPECIFIC V2 (0x72) : MANUFACTURER_SPECIFIC_REPORT )
 *
 *  Manufacturer-Specific Reports are used to advertise manufacturer-specific information, such as product number
 *  and serial number.
 *
 *  Action: Publish values as device 'data'. Log a warn message if manufacturerId and/or productId do not
 *  correspond to Fibaro Dimmer 2.
 *
 *  Example: ManufacturerSpecificReport(manufacturerId: 271, manufacturerName: Fibargroup, productId: 4096,
 *   productTypeId: 258)
 **/
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
    logger("zwaveEvent(): Manufacturer-Specific Report received: ${cmd}","trace")

    // Display as hex strings:
    def manufacturerIdDisp = String.format("%04X",cmd.manufacturerId)
    def productIdDisp = String.format("%04X",cmd.productId)
    def productTypeIdDisp = String.format("%04X",cmd.productTypeId)

    logger("Manufacturer-Specific Report: Manufacturer ID: ${manufacturerIdDisp}, Manufacturer Name: ${cmd.manufacturerName}" +
    ", Product Type ID: ${productTypeIdDisp}, Product ID: ${productIdDisp}","info")

    if ( 271 != cmd.manufacturerId) logger("Device Manufacturer is not Fibaro. Using this device handler with a different device may damage your device!","warn")
    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")

    updateDataValue("manufacturerName",cmd.manufacturerName)
    updateDataValue("manufacturerId",manufacturerIdDisp)
    updateDataValue("productId",productIdDisp)
    updateDataValue("productTypeId",productTypeIdDisp)
}

/**
 *  zwaveEvent( COMMAND_CLASS_POWERLEVEL V1 (0x73) : POWERLEVEL_REPORT )
 *
 *  The Powerlevel Report is used to advertise the current RF transmit power of the device.
 *
 *  Action: Log an info message.
 *
 *  cmd attributes:
 *    Short  powerLevel  The current power level indicator value in effect on the node
 *    Short  timeout     The time in seconds the node has at Power level before resetting to normal Power level.
 *
 *  Example: PowerlevelReport(powerLevel: 0, timeout: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.powerlevelv1.PowerlevelReport cmd) {
    logger("zwaveEvent(): Powerlevel Report received: ${cmd}","trace")
    def power = (cmd.powerLevel > 0) ? "minus${cmd.powerLevel}dBm" : "NormalPower"
    logger("Powerlevel Report: Power: ${power}, Timeout: ${cmd.timeout}","info")
}

/**
 *  zwaveEvent( COMMAND_CLASS_PROTECTION V2 (0x75) : PROTECTION_REPORT )
 *
 *  The Protection Report is used to report the protection state of a device.
 *  I.e. measures to prevent unintentional control (e.g. by a child).
 *
 *  Action: Cache values, update syncPending, and log an info message.
 *
 *  cmd attributes:
 *    Short  localProtectionState  Local protection state (i.e. physical switches/buttons)
 *    Short  rfProtectionState     RF protection state.
 *
 *  Example: ProtectionReport(localProtectionState: 0, reserved01: 0, reserved11: 0, rfProtectionState: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.protectionv2.ProtectionReport cmd) {
    logger("zwaveEvent(): Protection Report received: ${cmd}","trace")

    state.protectLocalCache = cmd.localProtectionState
    state.protectRFCache = cmd.rfProtectionState

    def lp, rfp = ""

    switch(cmd.localProtectionState)  {
        case 0:
            lp = "Unprotected"
            break
        case 1:
            lp = "Protection by sequence"
            break
        case 2:
            lp = "No operation possible"
            break
        default:
            lp = "Unknwon"
            break

    }

    switch(cmd.rfProtectionState)  {
        case 0:
            rfp = "Unprotected"
            break
        case 1:
            rfp = "No RF Control"
            break
        case 2:
            rfp = "No RF Response"
            break
        default:
            rfp = "Unknwon"
            break
    }

    logger("Protection Report: Local Protection: ${lp}, RF Protection: ${rfp}","info")
    updateSyncPending()
}

/**
 *  zwaveEvent( COMMAND_CLASS_FIRMWARE_UPDATE_MD V2 (0x7A) : FirmwareMdReport )
 *
 *  The Firmware Meta Data Report Command is used to advertise the status of the current firmware in the device.
 *
 *  Action: Publish values as device 'data' and log an info message. No check is performed.
 *
 *  cmd attributes:
 *    Integer  checksum        Checksum of the firmware image.
 *    Integer  firmwareId      Firware ID (this is not the firmware version).
 *    Integer  manufacturerId  Manufacturer ID.
 *
 *  Example: FirmwareMdReport(checksum: 50874, firmwareId: 274, manufacturerId: 271)
 **/
def zwaveEvent(physicalgraph.zwave.commands.firmwareupdatemdv2.FirmwareMdReport cmd) {
    logger("zwaveEvent(): Firmware Metadata Report received: ${cmd}","trace")

    // Display as hex strings:
    def firmwareIdDisp = String.format("%04X",cmd.firmwareId)
    def checksumDisp = String.format("%04X",cmd.checksum)

    logger("Firmware Metadata Report: Firmware ID: ${firmwareIdDisp}, Checksum: ${checksumDisp}","info")

    updateDataValue("firmwareId","${firmwareIdDisp}")
    updateDataValue("firmwareChecksum","${checksumDisp}")
}

/**
 *  zwaveEvent( COMMAND_CLASS_ASSOCIATION V2 (0x85) : ASSOCIATION_REPORT )
 *
 *  The Association Report command is used to advertise the current destination nodes of a given association group.
 *
 *  Action: Log info message only. Do not cache values as the Fibaro Dimmer 2 uses COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION.
 *
 *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this
 *  is not possible due to security restrictions in the SmartThings platform.
 *
 *  Example: AssociationReport(groupingIdentifier: 4, maxNodesSupported: 5, nodeId: [1], reportsToFollow: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {
    logger("zwaveEvent(): Association Report received: ${cmd}","trace")

    //state."assocGroupCache${cmd.groupingIdentifier}" = cmd.nodeId

    // Display to user in hex format (same as IDE):
    def hexArray  = []
    cmd.nodeId.sort().each { hexArray.add(String.format("%02X", it)) };
    logger("Association Group ${cmd.groupingIdentifier} contains nodes: ${hexArray} (hexadecimal format)","info")

    //updateSyncPending()
}

/**
 *  zwaveEvent( COMMAND_CLASS_VERSION V1 (0x86) : VERSION_REPORT )
 *
 *  The Version Report Command is used to advertise the library type, protocol version, and application version.

 *  Action: Publish values as device 'data' and log an info message. No check is performed.
 *
 *  Note: Device actually supports V2, but SmartThings only supports V1.
 *
 *  cmd attributes:
 *    Short  applicationSubVersion
 *    Short  applicationVersion
 *    Short  zWaveLibraryType
 *    Short  zWaveProtocolSubVersion
 *    Short  zWaveProtocolVersion
 *
 *  Example: VersionReport(applicationSubVersion: 4, applicationVersion: 3, zWaveLibraryType: 3,
 *   zWaveProtocolSubVersion: 5, zWaveProtocolVersion: 4)
 **/
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
    logger("zwaveEvent(): Version Report received: ${cmd}","trace")

    def zWaveLibraryTypeDisp  = String.format("%02X",cmd.zWaveLibraryType)
    def zWaveLibraryTypeDesc  = ""
    switch(cmd.zWaveLibraryType) {
        case 1:
            zWaveLibraryTypeDesc = "Static Controller"
            break

        case 2:
            zWaveLibraryTypeDesc = "Controller"
            break

        case 3:
            zWaveLibraryTypeDesc = "Enhanced Slave"
            break

        case 4:
            zWaveLibraryTypeDesc = "Slave"
            break

        case 5:
            zWaveLibraryTypeDesc = "Installer"
            break

        case 6:
            zWaveLibraryTypeDesc = "Routing Slave"
            break

        case 7:
            zWaveLibraryTypeDesc = "Bridge Controller"
            break

        case 8:
            zWaveLibraryTypeDesc = "Device Under Test (DUT)"
            break

        case 0x0A:
            zWaveLibraryTypeDesc = "AV Remote"
            break

        case 0x0B:
            zWaveLibraryTypeDesc = "AV Device"
            break

        default:
            zWaveLibraryTypeDesc = "N/A"
    }

    def applicationVersionDisp = String.format("%d.%02d",cmd.applicationVersion,cmd.applicationSubVersion)
    def zWaveProtocolVersionDisp = String.format("%d.%02d",cmd.zWaveProtocolVersion,cmd.zWaveProtocolSubVersion)

    logger("Version Report: Application Version: ${applicationVersionDisp}, " +
           "Z-Wave Protocol Version: ${zWaveProtocolVersionDisp}, " +
           "Z-Wave Library Type: ${zWaveLibraryTypeDisp} (${zWaveLibraryTypeDesc})","info")

    updateDataValue("applicationVersion","${cmd.applicationVersion}")
    updateDataValue("applicationSubVersion","${cmd.applicationSubVersion}")
    updateDataValue("zWaveLibraryType","${zWaveLibraryTypeDisp}")
    updateDataValue("zWaveProtocolVersion","${cmd.zWaveProtocolVersion}")
    updateDataValue("zWaveProtocolSubVersion","${cmd.zWaveProtocolSubVersion}")
}

/**
 *  zwaveEvent( COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION V2 (0x8E) : ASSOCIATION_REPORT )
 *
 *  The Multi-channel Association Report command is used to advertise the current destinations of a given
 *  association group (nodes and endpoints).
 *
 *  Action: Store the destinations in the assocGroup cache, update syncPending, and log an info message.
 *
 *  Note: Ideally, we want to update the corresponding preference value shown on the Settings GUI, however this
 *  is not possible due to security restrictions in the SmartThings platform.
 *
 *  Example: MultiChannelAssociationReport(groupingIdentifier: 2, maxNodesSupported: 8, nodeId: [9,0,1,1,2,3],
 *            reportsToFollow: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) {
    logger("zwaveEvent(): Multi-Channel Association Report received: ${cmd}","trace")

    state."assocGroupCache${cmd.groupingIdentifier}" = cmd.nodeId // Must not sort as order is important.

    def assocGroupName = getAssocGroupsMd().find( { it.id == cmd.groupingIdentifier} ).name
    // Display to user in hex format (same as IDE):
    def hexArray  = []
    cmd.nodeId.each { hexArray.add(String.format("%02X", it)) };
    logger("Association Group #${cmd.groupingIdentifier} [${assocGroupName}] contains destinations: ${hexArray}","info")

    updateSyncPending()
}

/**
 *  zwaveEvent( COMMAND_CLASS_SECURITY V1 (0x98) : SECURITY_COMMANDS_SUPPORTED_REPORT )
 *
 *  The Security Commands Supported Report command advertises which command classes are supported using security
 *  encapsulation.
 *
 *  Action: Store the list of supported command classes in state.secureCommandClasses. Log info message.
 *
 *  Example:  SecurityCommandsSupportedReport(commandClassControl: [43],
 *   commandClassSupport: [32, 90, 133, 38, 142, 96, 112, 117, 39], reportsToFollow: 0)
 **/
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
    logger("zwaveEvent(): Security Commands Supported Report received: ${cmd}","trace")

    state.secureCommandClasses = cmd.commandClassSupport

    // Display to user in hex format (same as IDE):
    def hexArray  = []
    cmd.commandClassSupport.sort().each { hexArray.add(String.format("0x%02X", it)) };
    logger("Security Commands Supported: ${hexArray}","info")
}

/**
 *  zwaveEvent( COMMAND_CLASS_SECURITY V1 (0x98) : SECURITY_MESSAGE_ENCAPSULATION )
 *
 *  The Security Message Encapsulation command is used to encapsulate Z-Wave commands using AES-128.
 *
 *  Action: Extract the encapsulated command and pass to the appropriate zwaveEvent() handler.
 *
 *  cmd attributes:
 *    List<Short> commandByte         Parameters of the encapsulated command.
 *    Short   commandClassIdentifier  Command Class ID of the encapsulated command.
 *    Short   commandIdentifier       Command ID of the encapsulated command.
 *    Boolean secondFrame             Indicates if first or second frame.
 *    Short   sequenceCounter
 *    Boolean sequenced               True if the command is transmitted using multiple frames.
 **/
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
    logger("zwaveEvent(): Security Encapsulated Command received: ${cmd}","trace")

    def encapsulatedCommand = cmd.encapsulatedCommand(getCommandClassVersions())
    if (encapsulatedCommand) {
        return zwaveEvent(encapsulatedCommand)
    } else {
        logger("zwaveEvent(): Unable to extract security encapsulated command from: ${cmd}","error")
    }
}

/**
 *  zwaveEvent( DEFAULT CATCHALL )
 *
 *  Called for all commands that aren't handled above.
 **/
def zwaveEvent(physicalgraph.zwave.Command cmd) {
    logger("zwaveEvent(): No handler for command: ${cmd}","error")
}


/*****************************************************************************************************************
 *  Capability-related Commands:
 *****************************************************************************************************************/

/**
 *  on()                        [Capability: Switch]
 *
 *  Turn the dimmer on.
 **/
def on() {
    logger("on(): Turning dimmer on.","info")
    def cmds = []
    cmds << zwave.basicV1.basicSet(value: 0xFF)
    if (state.proactiveReports) cmds << zwave.switchMultilevelV1.switchMultilevelGet()
    sendSecureSequence(cmds,5000)
}

/**
 *  off()                       [Capability: Switch]
 *
 *  Turn the dimmer off.
 **/
def off() {
    logger("off(): Turning dimmer off.","info")
    def cmds = []
    cmds << zwave.basicV1.basicSet(value: 0x00)
    if (state.proactiveReports) cmds << zwave.switchMultilevelV1.switchMultilevelGet()
    sendSecureSequence(cmds,5000)
}

/**
 *  setLevel()                  [Capability: Switch Level]
 *
 *  Set the dimmer level.
 *
 *  Parameters:
 *   level    Target level (0-100%).
 **/
def setLevel(level) {
    logger("setLevel(${level})","trace")

    if (level < 0) level = 0
    if (level > 100) level = 100
    logger("Setting dimmer to ${level}%","info")

    // Clear nightmodePendingLevel as it's been overridden.
    state.nightmodePendingLevel = 0

    def cmds = []
    cmds << zwave.basicV1.basicSet(value: Math.round(level * 99 / 100 )) // Convert from 0-100 to 0-99.
    if (state.proactiveReports) cmds << zwave.switchMultilevelV1.switchMultilevelGet()
    sendSecureSequence(cmds,5000)
}

/**
 *  refresh()                   [Capability: Refresh]
 *
 *  Request switchMultilevel, energy, and power reports.
 *  Also, force a configuration sync.
 **/
def refresh() {
    logger("refresh()","trace")

    def cmds = []
    cmds << zwave.switchMultilevelV1.switchMultilevelGet()
    cmds << zwave.meterV3.meterGet(scale: 0)
    cmds << zwave.meterV3.meterGet(scale: 2)

    sendSecureSequence(cmds,200)
    sync()
}

/**
 *  poll()                      [Capability: Polling]
 *
 *  Calls refresh().
 **/
def poll() {
    logger("poll()","trace")
    refresh()
}

/*****************************************************************************************************************
 *  Custom Commands:
 *****************************************************************************************************************/

/**
 *  reset()
 *
 *  Calls resetEnergy().
 *
 *  Note: this used to be part of the official 'Energy Meter' capability, but isn't anymore.
 **/
def reset() {
    logger("reset()","trace")
    resetEnergy()
}

/**
 *  resetEnergy()
 *
 *  Reset the Accumulated Energy figure held in the device.
 *
 *  Does not return a list of commands, it sends them immediately using sendSecureSequence(). This is required if
 *  triggered by schedule().
 **/
def resetEnergy() {
    logger("resetEnergy(): Resetting Accumulated Energy","info")

    state.energyLastReset = new Date().format("YYYY/MM/dd \n HH:mm:ss", location.timeZone)
    sendEvent(name: "energyLastReset", value: state.energyLastReset, descriptionText: "Accumulated Energy Reset")

    sendSecureSequence([
        zwave.meterV3.meterReset(),
        zwave.meterV3.meterGet(scale: 0)
    ],400)
}

/**
 *  enableNightmode(level)
 *
 *  Force switch-on illuminance level.
 *
 *  Does not return a list of commands, it sends them immediately using sendSecureSequence(). This is required if
 *  triggered by schedule().
 **/
def enableNightmode(level=-1) {
    logger("enableNightmode(${level})","info")

    // Clean level value:
    if (level == -1) level = settings.configNightmodeLevel.toInteger()
    if (level > 100) level = 100
    if (level < 1) level = 1

    // If nightmode is not already active, save last active level and current value of param19, so they can be restored when nightmode is stopped:
    if (!state.nightmodeActive) {

        state.nightmodePriorLevel = state.lastActiveLevel
        logger("enableNightmode(): Saved previous active level: ${state.nightmodePriorLevel}","info")

        if (!state.paramCache19) state.paramCache19 = 0
        state.nightmodePriorParam19 = state.paramCache19.toInteger()
        logger("enableNightmode(): Saved previous param19: ${state.paramCache19}","info")
    }

    // If the dimmer is already on, and configNightmodeForce is enabled, then adjust the level immediately:
    if (("on" == device.latestValue("switch")) & ("true" == configNightmodeForce)) sendSecureSequence([zwave.basicV1.basicSet(value: Math.round(level * 99 / 100 ))])

    state.nightmodeActive = true
    sendEvent(name: "nightmode", value: "Enabled", descriptionText: "Nightmode Enabled", isStateChange: true)

    // Update parameter #19 for force next switch-on level:
    state.paramTarget19 = level.toInteger()
    sync()
}

/**
 *  disableNightmode()
 *
 *  Stop nightmode and restore previous values.
 *
 *  Does not return a list of commands, it sends them immediately using sendSecureSequence(). This is required if
 *  triggered by schedule().
 **/
def disableNightmode() {
    logger("disableNightmode()","info")

    // If nightmode is active, restore param19:
    if (state.nightmodeActive) {

        logger("disableNightmode(): Restoring previous value of param19 to: ${state.nightmodePriorParam19}","debug")
        state.paramTarget19 = state.nightmodePriorParam19
        sync()

        if (state.nightmodePriorLevel > 0) {
            if (("on" == device.latestValue("switch")) & ("true" == configNightmodeForce)) {
                // Dimmer is already on and configNightmodeForce is enabled, so adjust the level immediately:
                logger("disableNightmode(): Restoring level to: ${state.nightmodePriorLevel}","debug")
                sendSecureSequence([zwave.basicV1.basicSet(value: Math.round(state.nightmodePriorLevel.toInteger() * 99 / 100 ))])
            } else if (0 == state.nightmodePriorParam19) {
                // 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.
                logger("disableNightmode(): Setting flag to restore level at next switch-on: ${state.nightmodePriorLevel}","debug")
                state.nightmodePendingLevel = state.nightmodePriorLevel
            }
        }
    }

    state.nightmodeActive = false
    sendEvent(name: "nightmode", value: "Disabled", descriptionText: "Nightmode Disabled", isStateChange: true)
}

/**
 *  toggleNightmode()
 **/
def toggleNightmode() {
    logger("toggleNightmode()","trace")

    if (state.nightmodeActive) {
        disableNightmode()
    }
    else {
        enableNightmode(configNightmodeLevel)
    }
}

/**
 *  clearFault()
 *
 *  Clear all active faults.
 **/
def clearFault() {
    logger("clearFault(): Clearing active faults.","info")
    sendEvent(name: "fault", value: "clear", descriptionText: "Fault cleared", displayed: true)
}

/*****************************************************************************************************************
 *  SmartThings System Commands:
 *****************************************************************************************************************/

/**
 *  installed()
 *
 *  Runs when the device is first installed.
 *
 *  Action: Set initial values for internal state, and request a full configuration report from the device.
 **/
def installed() {
    log.trace "installed()"

    state.installedAt = now()
    state.energyLastReset = new Date().format("YYYY/MM/dd \n HH:mm:ss", location.timeZone)
    state.loggingLevelIDE     = 3
    state.loggingLevelDevice  = 2
    state.protectLocalTarget  = 0
    state.protectRFTarget     = 0

    sendEvent(name: "fault", value: "clear", descriptionText: "Fault cleared", displayed: false)

    refreshConfig()
}

/**
 *  updated()
 *
 *  Runs when the user hits "Done" from Settings page.
 *
 *  Action: Process new settings, sync parameters and association group members with the physical device. Request
 *  Firmware Metadata, Manufacturer-Specific, and Version reports.
 *
 *  Note: Weirdly, update() seems to be called twice. So execution is aborted if there was a previous execution
 *  within two seconds. See: https://community.smartthings.com/t/updated-being-called-twice/62912
 **/
def updated() {
    logger("updated()","trace")

    def cmds = []

    if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {
        state.updatedLastRanAt = now()

        // Update internal state:
        state.loggingLevelIDE     = settings.configLoggingLevelIDE.toInteger()
        state.loggingLevelDevice  = settings.configLoggingLevelDevice.toInteger()
        state.syncAll             = ("true" == settings.configSyncAll)
        state.proactiveReports    = ("true" == settings.configProactiveReports)

        // Manage Schedules:
        manageSchedules()

        // Update Parameter target values:
        getParamsMd().findAll( {!it.readonly} ).each { // Exclude readonly parameters.
            state."paramTarget${it.id}" = settings."configParam${it.id}"?.toInteger()
        }

        // Check if auto-calibration is being forced. If so, must ignore target values for P1/2/30:
        if (state.paramTarget13 > 0) {
            state.paramCache13 = null // Remove cached value to force sync of P13:
            logger("Auto-calibration is being forced.","info")
            if (state.paramTarget1 != null) logger("Auto-calibration is being forced, but a value has been " +
            "provided for parameter #1. This will be ignored! Check Live Logging for the auto-calibrated " +
            "value shortly.","warn")
            if (state.paramTarget2 != null) logger("Auto-calibration is being forced, but a value has been " +
            "provided for parameter #2. This will be ignored! Check Live Logging for the auto-calibrated " +
            "value shortly.","warn")
            if (state.paramTarget30 != null) logger("Auto-calibration is being forced, but a value has been " +
            "provided for parameter #30. This will be ignored! Check Live Logging for the auto-calibrated " +
            "value shortly.","warn")
            state.paramTarget1 = null
            state.paramTarget2 = null
            state.paramTarget30 = null
        }

        // Update Assoc Group target values:
        state.assocGroupTarget1 = [ zwaveHubNodeId ] // Assoc Group #1 is Lifeline and will contain controller only.
        getAssocGroupsMd().findAll( { it.id != 1} ).each {
            state."assocGroupTarget${it.id}" = parseAssocGroupInput(settings."configAssocGroup${it.id}", it.maxNodes)
        }

        // Update Protection target values:
        state.protectLocalTarget = settings.configProtectLocal.toInteger()
        state.protectRFTarget    = settings.configProtectRF.toInteger()

        // Sync configuration with phyiscal device:
        sync(state.syncAll)

        // Set target for parameter #13 [Force Auto-calibration] back to 0 [Readout].
        // Sync will now only complete when auto-calibration has completed:
        state.paramTarget13 = 0

        // Request device medadata (this just seems the best place to do it):
        cmds << zwave.firmwareUpdateMdV2.firmwareMdGet()
        cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet()
        cmds << zwave.powerlevelV1.powerlevelGet()
        cmds << zwave.versionV1.versionGet()

        return response(secureSequence(cmds))
    }
    else {
        logger("updated(): Ran within last 2 seconds so aborting.","debug")
    }
}

/*****************************************************************************************************************
 *  Private Helper Functions:
 *****************************************************************************************************************/

/**
 *  logger()
 *
 *  Wrapper function for all logging:
 *    Logs messages to the IDE (Live Logging), and also keeps a historical log of critical error and warning
 *    messages by sending events for the device's logMessage attribute.
 *    Configured using configLoggingLevelIDE and configLoggingLevelDevice preferences.
 **/
private logger(msg, level = "debug") {

    switch(level) {
        case "error":
            if (state.loggingLevelIDE >= 1) log.error msg
            if (state.loggingLevelDevice >= 1) sendEvent(name: "logMessage", value: "ERROR: ${msg}", displayed: false, isStateChange: true)
            break

        case "warn":
            if (state.loggingLevelIDE >= 2) log.warn msg
            if (state.loggingLevelDevice >= 2) sendEvent(name: "logMessage", value: "WARNING: ${msg}", displayed: false, isStateChange: true)
            break

        case "info":
            if (state.loggingLevelIDE >= 3) log.info msg
            break

        case "debug":
            if (state.loggingLevelIDE >= 4) log.debug msg
            break

        case "trace":
            if (state.loggingLevelIDE >= 5) log.trace msg
            break

        default:
            log.debug msg
            break
    }
}

/**
 *  parseAssocGroupInput(string, maxNodes)
 *
 *  Converts a comma-delimited string of destinations (nodes and endpoints) into an array suitable for passing to
 *  multiChannelAssociationSet(). All numbers are interpreted as hexadecimal. Anything that's not a valid node or
 *  endpoint is discarded (warn). If the list has more than maxNodes, the extras are discarded (warn).
 *
 *  Example input strings:
 *    "9,A1"      = Nodes: 9 & 161 (no multi-channel endpoints)            => Output: [9, 161]
 *    "7,8:1,8:2" = Nodes: 7, Endpoints: Node8:endpoint1 & node8:endpoint2 => Output: [7, 0, 8, 1, 8, 2]
 */
private parseAssocGroupInput(string, maxNodes) {
    logger("parseAssocGroupInput(): Parsing Association Group Nodes: ${string}","trace")

    // First split into nodes and endpoints. Count valid entries as we go.
    if (string) {
        def nodeList = string.split(',')
        def nodes = []
        def endpoints = []
        def count = 0

        nodeList = nodeList.each { node ->
            node = node.trim()
            if ( count >= maxNodes) {
                logger("parseAssocGroupInput(): Number of nodes and endpoints is greater than ${maxNodes}! The following node was discarded: ${node}","warn")
            }
            else if (node.matches("\\p{XDigit}+")) { // There's only hexadecimal digits = nodeId
                def nodeId = Integer.parseInt(node,16)  // Parse as hex
                if ( (nodeId > 0) & (nodeId < 256) ) { // It's a valid nodeId
                    nodes << nodeId
                    count++
                }
                else {
                    logger("parseAssocGroupInput(): Invalid nodeId: ${node}","warn")
                }
            }
            else if (node.matches("\\p{XDigit}+:\\p{XDigit}+")) { // endpoint e.g. "0A:2"
                def endpoint = node.split(":")
                def nodeId = Integer.parseInt(endpoint[0],16) // Parse as hex
                def endpointId = Integer.parseInt(endpo
Download .txt
gitextract_e8liau_6/

├── LICENSE
├── README.md
├── devices/
│   ├── aeon-home-energy-meter/
│   │   └── aeon-home-energy-meter.groovy
│   ├── evohome/
│   │   └── evohome-heating-zone.groovy
│   ├── fibaro-dimmer-2/
│   │   ├── README.md
│   │   └── fibaro-dimmer-2.groovy
│   ├── fibaro-flood-sensor/
│   │   ├── README.md
│   │   └── fibaro-flood-sensor.groovy
│   ├── fibaro-rgbw-controller/
│   │   ├── README.md
│   │   └── fibaro-rgbw-controller.groovy
│   ├── greenwave-powernode-single/
│   │   ├── README.md
│   │   └── greenwave-powernode-single.groovy
│   ├── philio-dual-relay/
│   │   └── philio-dual-relay.groovy
│   ├── tkb-metering-switch/
│   │   └── tkb-metering-switch.groovy
│   └── zwave-tweaker/
│       ├── README.md
│       └── zwave-tweaker.groovy
└── smartapps/
    ├── evohome-connect/
    │   └── evohome-connect.groovy
    └── influxdb-logger/
        ├── README.md
        └── influxdb-logger.groovy
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (862K chars).
[
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 8857,
    "preview": "# SmartThings\nhttps://github.com/codersaur/SmartThings\n\nCopyright (c) 2017 [David Lomas](https://github.com/codersaur)\n\n"
  },
  {
    "path": "devices/aeon-home-energy-meter/aeon-home-energy-meter.groovy",
    "chars": 47140,
    "preview": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: Aeon Home Energy Meter (GEN2 - UK - 1 Clamp)\n *\n *  Author: "
  },
  {
    "path": "devices/evohome/evohome-heating-zone.groovy",
    "chars": 35681,
    "preview": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: Evohome Heating Zone\n *\n *  Author: David Lomas (codersaur)\n"
  },
  {
    "path": "devices/fibaro-dimmer-2/README.md",
    "chars": 18411,
    "preview": "# Fibaro Dimmer 2 (FGD-212)\nhttps://github.com/codersaur/SmartThings/tree/master/devices/fibaro-dimmer-2\n\nCopyright (c) "
  },
  {
    "path": "devices/fibaro-dimmer-2/fibaro-dimmer-2.groovy",
    "chars": 110493,
    "preview": "/*****************************************************************************************************************\n *  C"
  },
  {
    "path": "devices/fibaro-flood-sensor/README.md",
    "chars": 8363,
    "preview": "# Fibaro Flood Sensor (FGFS-101) (EU)\nhttps://github.com/codersaur/SmartThings/tree/master/devices/fibaro-flood-sensor\n\n"
  },
  {
    "path": "devices/fibaro-flood-sensor/fibaro-flood-sensor.groovy",
    "chars": 70402,
    "preview": "/*****************************************************************************************************************\n *  C"
  },
  {
    "path": "devices/fibaro-rgbw-controller/README.md",
    "chars": 14972,
    "preview": "# Fibaro RGBW Controller (FGRGBWM-441)\nhttps://github.com/codersaur/SmartThings/tree/master/devices/fibaro-rgbw-controll"
  },
  {
    "path": "devices/fibaro-rgbw-controller/fibaro-rgbw-controller.groovy",
    "chars": 108482,
    "preview": "/**\n *  Copyright David Lomas (codersaur)\n *\n *  SmartThings Device Handler for: Fibaro RGBW Controller EU v2.x (FGRGBWM"
  },
  {
    "path": "devices/greenwave-powernode-single/README.md",
    "chars": 11155,
    "preview": "# GreenWave PowerNode (Single) (NS210-G-EN)\nhttps://github.com/codersaur/SmartThings/tree/master/devices/greenwave-power"
  },
  {
    "path": "devices/greenwave-powernode-single/greenwave-powernode-single.groovy",
    "chars": 66446,
    "preview": "/*****************************************************************************************************************\n *  C"
  },
  {
    "path": "devices/philio-dual-relay/philio-dual-relay.groovy",
    "chars": 46974,
    "preview": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: Philio Dual Relay (PAN04) Single Mode\n *\n *  Author: David L"
  },
  {
    "path": "devices/tkb-metering-switch/tkb-metering-switch.groovy",
    "chars": 49535,
    "preview": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: TKB Metering Switch (TZ88E-GEN5)\n *\n *  Author: David Lomas "
  },
  {
    "path": "devices/zwave-tweaker/README.md",
    "chars": 17373,
    "preview": "# Z-Wave Tweaker\nhttps://github.com/codersaur/SmartThings/tree/master/devices/zwave-tweaker\n\nCopyright (c) [David Lomas]"
  },
  {
    "path": "devices/zwave-tweaker/zwave-tweaker.groovy",
    "chars": 106433,
    "preview": "/*****************************************************************************************************************\n *  C"
  },
  {
    "path": "smartapps/evohome-connect/evohome-connect.groovy",
    "chars": 46708,
    "preview": "/**\n *  Copyright 2016 David Lomas (codersaur)\n *\n *  Name: Evohome (Connect)\n *\n *  Author: David Lomas (codersaur)\n *\n"
  },
  {
    "path": "smartapps/influxdb-logger/README.md",
    "chars": 5728,
    "preview": "# InfluxDB Logger\n\nCopyright (c) [David Lomas](https://github.com/codersaur)\n\n## Overview\n\nThis SmartApp logs SmartThing"
  },
  {
    "path": "smartapps/influxdb-logger/influxdb-logger.groovy",
    "chars": 36981,
    "preview": "/*****************************************************************************************************************\n *  C"
  }
]

About this extraction

This page contains the full source code of the codersaur/SmartThings GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (802.2 KB), approximately 204.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!