Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICP-7174 ICP-7282 ICP-7283 ICP-7308 ICP-7309 ICP-7288 ICP-7289 Misc Stelpro fixes #3750

Merged
merged 14 commits into from Jan 3, 2019
Merged
Expand Up @@ -141,6 +141,7 @@ def setupHealthCheck() {

def configureSupportedRanges() {
sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
// These are part of the deprecated Thermostat capability. Remove these when that capability is removed.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am checking to see if OneApp uses this in anyway. It didn't seem like its presence affected the range displayed in OneApp, but I want to make sure it isn't needed because of the reference to "Thermostat" in the capabilities.

This applies to all three device handlers.

sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false)
sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false)
}
Expand Down Expand Up @@ -170,6 +171,10 @@ def parse(String description) {
if (!device.currentValue("supportedThermostatModes")) {
configureSupportedRanges()
}
// Existing installations need the temperatureAlarm state initialized
if (device.currentValue("temperatureAlarm") == null) {
sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false)
}

if (description == "updated") {
return null
Expand Down Expand Up @@ -285,27 +290,37 @@ def zwaveEvent(sensormultilevelv3.SensorMultilevelReport cmd) {
def map = [:]

if (cmd.sensorType == sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1) {
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision)
map.unit = getTemperatureScale()
map.name = "temperature"
temp = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision)

temp = map.value
// The specific values checked below represent ambient temperature alarm indicators
if (temp == "32765") { // 0x7FFD
if (temp == 0x7ffd) { // Freeze Alarm
map.name = "temperatureAlarm"
map.value = "freeze"
map.unit = ""
} else if (temp == "32767") { // 0x7FFF
} else if (temp == 0x7fff) { // Overheat Alarm
map.name = "temperatureAlarm"
map.value = "heat"
map.unit = ""
} else if (temp == "-32768"){ // 0x8000
map.name = "temperatureAlarm"
map.value = "cleared"
map.unit = ""
} else if (temp == 0x8000) { // Temperature Sensor Error
map.descriptionText = "Received a temperature error"
} else {
tempfloat = (Math.round(temp.toFloat() * 2)) / 2
map.value = tempfloat
map.name = "temperature"
map.value = (Math.round(temp.toFloat() * 2)) / 2
map.unit = getTemperatureScale()


// Handle cases where we need to update the temperature alarm state given certain temperatures
// Account for a f/w bug where the freeze alarm doesn't trigger at 0C
if (map.value < (map.unit == "C" ? 0 : 32)) {
log.debug "EARLY FREEZE ALARM @ $map.value $map.unit (raw $intVal)"
sendEvent(name: "temperatureAlarm", value: "freeze")
}
// Overheat alarm doesn't trigger until 80C, but we'll start sending at 50C to match thermostat display
else if (map.value >= (map.unit == "C" ? 50 : 122)) {
log.debug "EARLY HEAT ALARM @ $map.value $map.unit (raw $intVal)"
sendEvent(name: "temperatureAlarm", value: "heat")
} else if (device.currentValue("temperatureAlarm") != "cleared") {
log.debug "CLEAR ALARM @ $map.value $map.unit (raw $intVal)"
sendEvent(name: "temperatureAlarm", value: "cleared")
}
}
} else if (cmd.sensorType == sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_RELATIVE_HUMIDITY_VERSION_2) {
map.value = cmd.scaledSensorValue
Expand Down
Expand Up @@ -163,13 +163,18 @@ def getModeMap() {[
"05":"eco"
]}

def getDutyCyclePeriod() {
15 // seconds
}

def setupHealthCheck() {
// Device-Watch simply pings if no device events received for 32min(checkInterval)
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
}

def configureSupportedRanges() {
sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false)
// These are part of the deprecated Thermostat capability. Remove these when that capability is removed.
sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false)
sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false)
}
Expand Down Expand Up @@ -238,37 +243,23 @@ def parse(String description) {
log.debug "Desc Map: $descMap"
if (descMap.clusterInt == THERMOSTAT_CLUSTER) {
if (descMap.attrInt == ATTRIBUTE_LOCAL_TEMP) {
def intVal = Integer.parseInt(descMap.value, 16)
map.name = "temperature"
map.unit = getTemperatureScale()
map.value = getTemperature(descMap.value)

if (intVal == 0x7ffd) { // 0x7FFD
map.name = "temperatureAlarm"
map.value = "freeze"
map.unit = ""
} else if (intVal == 0x7fff) { // 0x7FFF
map.name = "temperatureAlarm"
map.value = "heat"
map.unit = ""
} else if (intVal == 0x8000) { // 0x8000
map.name = null
map.value = null
map.descriptionText = "Received a temperature error"
} else if (intVal > 0x8000) {
map.value = -(Math.round(2*(655.36 - map.value))/2)
}

if (device.currentValue("temperatureAlarm") != "cleared" && map.name == "temperature") {
sendEvent(name: "temperatureAlarm", value: "cleared")
}
map = handleTemperature(descMap)
} else if (descMap.attrInt == ATTRIBUTE_HEAT_SETPOINT) {
def intVal = Integer.parseInt(descMap.value, 16)
if (intVal != 0x8000) { // 0x8000
// We receive 0x8000 when the thermostat is off
if (intVal != 0x8000) {
log.debug "HEATING SETPOINT"
map.name = "heatingSetpoint"
map.value = getTemperature(descMap.value)
map.unit = getTemperatureScale()
map.data = [heatingSetpointRange: heatingSetpointRange]

// Sometimes we don't get an updated operating state when going from heating -> idle with a setpoint just below ambient;
// so ask for the operating state, but wait 1.5 times the duty cycle period.
def ambientTemp = device.currentValue("temperature")
if (ambientTemp != null && map.value < ambientTemp) {
runIn((dutyCyclePeriod * 3) / 2 as int, updateOperatingState, [overwrite: true])
}
}
} else if (descMap.attrInt == ATTRIBUTE_SYSTEM_MODE) {
log.debug "MODE - ${descMap.value}"
Expand All @@ -282,6 +273,8 @@ def parse(String description) {
map.data = [supportedThermostatModes: supportedThermostatModes]
} else {
state.storedSystemMode = value
// Sometimes we don't get the final decision, so ask for it just in case
sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_SETPOINT_MODE, ["mfgCode": "0x1185"]))
}
// Right now this doesn't seem to happen -- regardless of the size field the value seems to be two bytes
/*if (descMap.size == "08") {
Expand Down Expand Up @@ -316,7 +309,10 @@ def parse(String description) {
map.value = "heating"
}

if (settings.heatdetails == "No") {
// If the user does not want to see the Idle and Heating events in the event history,
// don't show them. Otherwise, don't show them more frequently than 30 seconds.
if (settings.heatdetails == "No" ||
!secondsPast(device.currentState("thermostatOperatingState")?.getLastUpdated(), 30)) {
map.displayed = false
}
}
Expand All @@ -331,6 +327,46 @@ def parse(String description) {
return result
}

def handleTemperature(descMap) {
def map = [:]
def intVal = Integer.parseInt(descMap.value, 16)

// Handle special temperature flags where we need to change the event type
if (intVal == 0x7ffd) { // Freeze Alarm
map.name = "temperatureAlarm"
map.value = "freeze"
} else if (intVal == 0x7fff) { // Overheat Alarm
map.name = "temperatureAlarm"
map.value = "heat"
} else if (intVal == 0x8000) { // Temperature Sensor Error
map.descriptionText = "Received a temperature error"
} else {
if (intVal > 0x8000) { // Handle negative C (< 32F) readings
intVal = -(Math.round(2 * (65536 - intVal)) / 2)
}
map.name = "temperature"
map.value = getTemperature(intVal)
map.unit = getTemperatureScale()

// Handle cases where we need to update the temperature alarm state given certain temperatures
// Account for a f/w bug where the freeze alarm doesn't trigger at 0C
if (map.value < (map.unit == "C" ? 0 : 32)) {
log.debug "EARLY FREEZE ALARM @ $map.value $map.unit (raw $intVal)"
sendEvent(name: "temperatureAlarm", value: "freeze")
}
// Overheat alarm doesn't trigger until 80C, but we'll start sending at 50C to match thermostat display
else if (map.value >= (map.unit == "C" ? 50 : 122)) {
log.debug "EARLY HEAT ALARM @ $map.value $map.unit (raw $intVal)"
sendEvent(name: "temperatureAlarm", value: "heat")
} else if (device.currentValue("temperatureAlarm") != "cleared") {
log.debug "CLEAR ALARM @ $map.value $map.unit (raw $intVal)"
sendEvent(name: "temperatureAlarm", value: "cleared")
}
}

map
}

def updateWeather() {
log.debug "updating weather"
def weather
Expand Down Expand Up @@ -363,6 +399,10 @@ def scheduledUpdateWeather() {
}
}

def updateOperatingState() {
sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE))
}

/**
* PING is used by Device-Watch in attempt to reach the Device
**/
Expand All @@ -387,14 +427,24 @@ def poll() {
requests
}

/**
* Given a raw temperature reading in Celsius return a converted temperature.
*
* @param value The temperature in Celsius, treated based on the following:
* If value instanceof String, treat as a raw hex string and divide by 100
* Otherwise treat value as a number and divide by 100
*
* @return A Celsius or Farenheit value
*/
def getTemperature(value) {
if (value != null) {
log.debug("value $value")
def celsius = Integer.parseInt(value, 16) / 100
def celsius = (value instanceof String ? Integer.parseInt(value, 16) : value) / 100
if (getTemperatureScale() == "C") {
return celsius
} else {
return Math.round(celsiusToFahrenheit(celsius))
def rounded = new BigDecimal(celsiusToFahrenheit(celsius)).setScale(0, BigDecimal.ROUND_HALF_UP)
return rounded
}
}
}
Expand Down Expand Up @@ -519,7 +569,7 @@ def configure() {
requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT, DataType.INT16, 1, 0, 50)
requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_SYSTEM_MODE, DataType.ENUM8, 1, 0, 1)
requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_SETPOINT_MODE, DataType.ENUM8, 1, 0, 1)
requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE, DataType.UINT8, 300, 900, 5)
requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE, DataType.UINT8, 1, 900, 1)

// Configure Thermostat Ui Conf Cluster
requests += zigbee.configureReporting(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_TEMP_DISP_MODE, DataType.ENUM8, 1, 0, 1)
Expand Down Expand Up @@ -570,4 +620,26 @@ def fanAuto() {
log.debug "${device.displayName} does not support fan auto"
}

/**
* Checks if the time elapsed from the provided timestamp is greater than the number of senconds provided
*
* @param timestamp: The timestamp
*
* @param seconds: The number of seconds
*
* @returns true if elapsed time is greater than number of seconds provided, else false
*/
private Boolean secondsPast(timestamp, seconds) {
if (!(timestamp instanceof Number)) {
if (timestamp instanceof Date) {
timestamp = timestamp.time
} else if ((timestamp instanceof String) && timestamp.isNumber()) {
timestamp = timestamp.toLong()
} else {
return true
}
}
return (now() - timestamp) > (seconds * 1000)
}