/**
 *  Danfoss Living Connect Radiator Thermostat LC-13
 *
 *  Copyright 2017 Tom Philip
 *
 *  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.
 *
 *
 *  Revision History
 *  ==============================================
 *  2017-09-16 - Uses the 'official' colour range for showing off the temperature.
 *  2017-09-16 - Battery icon - Credit: Power bank by Gregor Cresnar from the Noun Project.
 *  2017-03-10 - Radiator can be turned off, whilst keeping the next temperature state. Credit: https://github.com/struna
 *
 */

metadata {
	definition (name: "Danfoss Living Connect Radiator Thermostat LC-13 v3.01", namespace: "tommysqueak", author: "Tom Philip") {
		capability "Actuator"
		capability "Sensor"
		capability "Thermostat"
		capability "Battery"
		capability "Configuration"
		capability "Switch"

		command "temperatureUp"
		command "temperatureDown"
		attribute "nextHeatingSetpoint", "number"

		// raw fingerprint zw:S type:0804 mfr:0002 prod:0005 model:0004 ver:1.01 zwv:3.67 lib:06 cc:80,46,81,72,8F,75,43,86,84 ccOut:46,81,8F
		fingerprint type: "0804", mfr: "0002", prod: "0005", model: "0004", cc: "80,46,81,72,8F,75,43,86,84", ccOut:"46,81,8F"
		// 0x80 = Battery v1
		// 0x46 = Climate Control Schedule v1
		// 0x81 = Clock v1
		// 0x72 = Manufacturer Specific v1
		// 0x8F = Multi Cmd v1 (Multi Command Encapsulated)
		// 0x75 = Protection v2
		// 0x43 = Thermostat Setpoint v2
		// 0x86 = Version v1
		// 0x84 = Wake Up v2
	}


	// http://scripts.3dgo.net/smartthings/icons/
	// TODO: create temp set like Nest thermostat - http://docs.smartthings.com/en/latest/device-type-developers-guide/tiles-metadata.html

	preferences {
		input "wakeUpIntervalInMins", "number", title: "Wake Up Interval (min). Default 5mins.", description: "Wakes up and send\receives new temperature setting", range: "1..30", displayDuringSetup: true
		input "quickOnTemperature", "number", title: "Quick On Temperature. Default 21°C.", description: "Quickly turn on the radiator to this temperature", range: "5..82", displayDuringSetup: false
		input "quickOffTemperature", "number", title: "Quick Off Temperature. Default 4°C.", description: "Quickly turn off the radiator to this temperature", range: "4..68", displayDuringSetup: false
		
		input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
		input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
	}
}

//	Event order
//	Scenario - temp is changed via the app
//	* BatteryReport (seem to get one of these everytime it wakes up! This is ood, as we don't ask for it)
//  * WakeUpNotification (the new temperature, set by the app is sent)
//  * ScheduleOverrideReport (we don't handle it)
//  * ThermostatSetpointReport (we receivce the new temp we sent when it woke up. We also set the heatingSetpoint and next one, all is aligned :))

//	Scenario - temp is changed via the app - alternative (this sometimes happens instead)
//	* ThermostatSetpointReport - resets the next temp back to the current temp. Not what we want :(
//	* BatteryReport
//	* WakeUpNotification
//	* ScheduleOverrideReport

//	Scenario - temp is changed on the radiator thermostat
//	* BatteryReport (seem to get one of these everytime it wakes up! This is ood, as we don't ask for it)
//  * ThermostatSetpointReport (we receive the temp set on the thermostat. We also set the heatingSetpoint and next one. If we set a next one, it's overwritten.)
//  * ScheduleOverrideReport (we don't handle it)
//  * WakeUpNotification (no temp is sent, as they're both the same coz of ThermostatSetpointReport)

//	Scenario - wakes up
//	* BatteryReport
//	* WakeUpNotification

//	WakeUpINterval 1800 (5mins)

//	defaultWakeUpIntervalSeconds: 300, maximumWakeUpIntervalSeconds: 1800 (30 mins),
//	minimumWakeUpIntervalSeconds: 60, wakeUpIntervalStepSeconds: 60

// All messages from the device are passed to the parse method.
// It is responsible for turning those messages into something the SmartThings platform can understand.

def logsOff(){
	log.warn "debug logging disabled..."
	device.updateSetting("logEnable",[value:"false",type:"bool"])
}

def updated(){
log.info "updated..."
	log.warn "debug logging is: ${logEnable == true}"
	log.warn "description logging is: ${txtEnable == true}"
	if (logEnable) runIn(1800,logsOff)
	response(configure())
}

	//if (txtEnable) log.info
	//if (logEnable) log.debug
def parse(String description) {
	if (logEnable) log.info "Parsing '${description}'"

	def result = null
	//	The commands in the array are to map to versions of the command class. eg hubitat.zwave.commands.wakeupv1 vs hubitat.zwave.commands.wakeupv2
	//	If none specified, it'll use the latest version of that command class.
	def cmd = zwave.parse(description)
	if (cmd) {
		result = zwaveEvent(cmd)
		if (logEnable) log.info "Parsed ${cmd} to ${result.inspect()}"
	}
	else {
		if (txtEnable) log.debug "Non-parsed event: ${description}"
	}
	result
}

//	catch all unhandled events
def zwaveEvent(hubitat.zwave.Command cmd) {
	return createEvent(descriptionText: "Uncaptured event for ${device.displayName}: ${cmd}")
}

def zwaveEvent(hubitat.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) {
	//	Parsed ThermostatSetpointReport(precision: 2, reserved01: 0, scale: 0, scaledValue: 21.00, setpointType: 1, size: 2, value: [8, 52])

	// So we can respond with same format later, see setHeatingSetpoint()
	state.scale = cmd.scale
	state.precision = cmd.precision

	def eventList = []
	def cmdScale = cmd.scale == 1 ? "F" : "C"
	def radiatorTemperature = Double.parseDouble(convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision)).round(1)

	def currentTemperature = currentDouble("heatingSetpoint")
	def nextTemperature = currentDouble("nextHeatingSetpoint")

	if (txtEnable) log.info "SetpointReport. current:${currentTemperature} next:${nextTemperature} radiator:${radiatorTemperature}"

	def deviceTempMap = [name: "heatingSetpoint", value: radiatorTemperature, unit: getTemperatureScale()]

	if(radiatorTemperature != currentTemperature){
		// The radiator temperature has changed. Why?
		// Is it because the app told it to change or is it coz it was done manually
		if(state.lastSentTemperature == radiatorTemperature) {
			def thermostatMode = device.currentValue("thermostatMode")
			// radiator is off
			if (thermostatMode == "off") {
				deviceTempMap.descriptionText = "Thermostat mode is off, temperature changed to ${radiatorTemperature}"
			}
			// it's the app! raise event heatingSetpoint, with desc App
			else {
				deviceTempMap.descriptionText = "Temperature changed by app to ${radiatorTemperature}"
			}
		}
		else {
			// It's manual? raise event heatingSetpoint, with desc Manual
			// And I think set the next to be the manual setting too. All aligned.
			deviceTempMap.descriptionText = "Temperature changed manually to ${radiatorTemperature}"
			state.lastSentTemperature = radiatorTemperature
			eventList << createEvent(name:"nextHeatingSetpoint", value: radiatorTemperature, unit: getTemperatureScale(), displayed: false)

			def offTemperature = quickOffTemperature ?: fromCelsiusToLocal(4)
			if (radiatorTemperature > offTemperature) {
				eventList << createEvent(name: "switch", value: "on", displayed: false)
				eventList << createEvent(name: "thermostatMode", value: "heat", descriptionText: "Thermostat mode is changed to heat")
				eventList << createEvent(name: "thermostatOperatingState", value: "heating", displayed: false)
			}
		}
	}

	eventList << createEvent(deviceTempMap)
	// For acting like a thermostat
	eventList << createEvent(name: "temperature", value: radiatorTemperature, unit: getTemperatureScale(), displayed: false)
	eventList << createEvent(name: "thermostatSetpoint", value: radiatorTemperature, unit: getTemperatureScale(), displayed: false)

	if(nextTemperature == 0) {
		// initialise the nextHeatingSetpoint, on the very first time we install and get the devices temp
		state.lastSentTemperature = radiatorTemperature
		eventList << createEvent(name:"nextHeatingSetpoint", value: radiatorTemperature, unit: getTemperatureScale(), displayed: false)
	}

	eventList
}

def zwaveEvent(hubitat.zwave.commands.wakeupv2.WakeUpNotification cmd) {
	if (txtEnable) log.info "Wakey wakey"

	def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false)
	def cmds = []

	// Only ask for battery if we haven't had a BatteryReport in a while
	if (!state.lastBatteryReportReceivedAt || (new Date().time) - state.lastBatteryReportReceivedAt > daysToTime(7)) {
		if (txtEnable) log.info "Asking for battery report"
		cmds << zwave.batteryV1.batteryGet().format()
	}

	// Send the new temperature, if we haven't yet sent it
	if (txtEnable) log.info "New temperature check. Next: ${device.currentValue("nextHeatingSetpoint")} vs Current: ${device.currentValue("heatingSetpoint")}"

	def nextHeatingSetpoint = currentDouble("nextHeatingSetpoint")
	def heatingSetpoint = currentDouble("heatingSetpoint")
	def thermostatMode = device.currentValue("thermostatMode")

	if (txtEnable) log.info "Thermostat mode is ${thermostatMode}"
	if (thermostatMode == "off") {
		nextHeatingSetpoint = quickOffTemperature ?: fromCelsiusToLocal(4)
	}

	if (nextHeatingSetpoint != 0 && nextHeatingSetpoint != heatingSetpoint) {
		if (txtEnable) log.info "Sending new temperature ${nextHeatingSetpoint}"
		state.lastSentTemperature = nextHeatingSetpoint
		cmds << setHeatingSetpointCommand(nextHeatingSetpoint).format()
		cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()
	}

	cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
	[event, response(delayBetween(cmds, 1500))]
}

def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
	def map = [ name: "battery", unit: "%" ]
	if (cmd.batteryLevel == 0xFF) {  // Special value for low battery alert
		map.value = 1
		map.descriptionText = "Low Battery"
	} else {
		map.value = cmd.batteryLevel
	}
	// Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
	state.lastBatteryReportReceivedAt = new Date().time
	createEvent(map)
}

//
//	commands (that it can handle, must implement those that match it's capabilities, so SmartApps can call these methods)
//
//	TODO: review the commands, do they have the right interface/params

def temperatureUp() {
	def nextTemp = currentDouble("nextHeatingSetpoint") + 0.5d
	// It can't handle above 28, so don't allow it go above
	// TODO: deal with Farenheit?
	if(nextTemp > fromCelsiusToLocal(28)) {
		nextTemp = fromCelsiusToLocal(28)
	}
	on()
	setHeatingSetpoint(nextTemp)
}

def temperatureDown() {
	def nextTemp = currentDouble("nextHeatingSetpoint") - 0.5d
	// It can't go below 4, so don't allow it
	if(nextTemp < fromCelsiusToLocal(4)) {
		nextTemp = fromCelsiusToLocal(4)
		off()
	}
	else {
		on()
	}
	setHeatingSetpoint(nextTemp)
}

//	Thermostat Heating Setpoint
def setHeatingSetpoint(degrees) {
	setHeatingSetpoint(degrees.toDouble())
}

def setHeatingSetpoint(Double degrees) {
	if (txtEnable) log.info "Storing temperature for next wake ${degrees}"

	sendEvent(name:"nextHeatingSetpoint", value: degrees.round(1), unit: getTemperatureScale(), descriptionText: "Next heating setpoint is ${degrees}")
}

def setHeatingSetpointCommand(Double degrees) {
	if (txtEnable) log.info "setHeatingSetpoint ${degrees}"
	def deviceScale = state.scale ?: 0
	def deviceScaleString = deviceScale == 1 ? "F" : "C"
	def locationScale = getTemperatureScale()
	def precision = state.precision ?: 2

	def convertedDegrees
	if (locationScale == "C" && deviceScaleString == "F") {
		convertedDegrees = celsiusToFahrenheit(degrees)
	} else if (locationScale == "F" && deviceScaleString == "C") {
		convertedDegrees = fahrenheitToCelsius(degrees)
	} else {
		convertedDegrees = degrees
	}

	zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: precision, scaledValue: convertedDegrees)
}

def on() {
	sendEvent(name: "switch", value: "on", displayed: false)
	sendEvent(name: "thermostatMode", value: "heat", descriptionText: "Thermostat mode is changed to heat")
	sendEvent(name: "thermostatOperatingState", value: "heating", displayed: false)

	def nextTemperature = currentDouble("nextHeatingSetpoint")
	def onTemperature = quickOnTemperature ?: fromCelsiusToLocal(21)
	def offTemperature = quickOffTemperature ?: fromCelsiusToLocal(4)
	if (nextTemperature == offTemperature) {
		setHeatingSetpoint(onTemperature)
	}
}

def off() {
	sendEvent(name: "switch", value: "off", displayed: false)
	sendEvent(name: "thermostatMode", value: "off", descriptionText: "Thermostat mode is changed to off")
	sendEvent(name: "thermostatOperatingState", value: "idle", displayed: false)
}


def setCoolingSetpoint(number) {
}

def heat() {
	on()
}

def cool() {
	off()
}

def emergencyHeat() {
	on()
	setHeatingSetpoint(fromCelsiusToLocal(10))
}

def setThermostatMode(mode) {
	sendEvent(name: "switch", value: (mode == "heat") ? "on" : "off", displayed: false)
	sendEvent(name: "thermostatMode", value: (mode == "heat") ? "heat" : "off", descriptionText: "Thermostat mode is changed to ${mode}")
	sendEvent(name: "thermostatOperatingState", value: (mode == "heat") ? "heating" : "idle", displayed: false)
}

def fanOn() {
}

def fanAuto() {
}

def fanCirculate() {
}

def setThermostatFanMode(mode) {
}

def auto() {
}

def installed() {
	if (txtEnable) log.info "installed"
	delayBetween([
		// Not sure if this is needed :/
		zwave.configurationV1.configurationSet(parameterNumber:1, size:2, scaledConfigurationValue:100).format(),
		// Get it's configured info, like it's scale (Celsius, Farenheit) Precicsion
		// 1 = SETPOINT_TYPE_HEATING_1
		zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(),
		zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(),
		// Set it's time/clock. Do we need to do this periodically, like the battery check?
		currentTimeCommand(),
		// Make sure sleepy battery-powered sensor sends its
		// WakeUpNotifications to the hub every 5 mins intially
		zwave.wakeUpV1.wakeUpIntervalSet(seconds:300, nodeid:zwaveHubNodeId).format()
	], 1000)
}


def configure() {
	if (txtEnable) log.info("configure")
	def wakeUpEvery = (wakeUpIntervalInMins ?: 5) * 60
	[
		zwave.wakeUpV1.wakeUpIntervalSet(seconds:wakeUpEvery, nodeid:zwaveHubNodeId).format()
	]
}

private setClock() {
	// set the clock once a week
	def now = new Date().time
	if (!state.lastClockSet || now - state.lastClockSet > daysToTime(7)) {
		state.lastClockSet = now
		currentTimeCommand()
	}
	else {
		null
	}
}

private daysToTime(days) {
	days*24*60*60*1000
}

private fromCelsiusToLocal(Double degrees) {
	if(getTemperatureScale() == "F") {
		return celsiusToFahrenheit(degrees)
	}
	else {
		return degrees
	}
}

private currentTimeCommand() {
	def nowCalendar = Calendar.getInstance(location.timeZone)
	if (txtEnable) log.info "Setting clock to ${nowCalendar.getTime().format("dd-MM-yyyy HH:mm z", location.timeZone)}"
	def weekday = nowCalendar.get(Calendar.DAY_OF_WEEK) - 1
	if (weekday == 0) {
		weekday = 7
	}
	zwave.clockV1.clockSet(hour: nowCalendar.get(Calendar.HOUR_OF_DAY), minute: nowCalendar.get(Calendar.MINUTE), weekday: weekday).format()
}

private currentDouble(attributeName) {
	if(device.currentValue(attributeName)) {
		return device.currentValue(attributeName).doubleValue()
	}
	else {
		return 0d
	}
}
