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 @@ -170,6 +170,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.
sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false)
sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false)
}
Expand Down Expand Up @@ -238,37 +239,19 @@ 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) {
state.rawSetpoint = intVal
log.debug "HEATING SETPOINT"
map.name = "heatingSetpoint"
map.value = getTemperature(descMap.value)
map.unit = getTemperatureScale()
map.data = [heatingSetpointRange: heatingSetpointRange]

handleOperatingStateBugfix()
}
} else if (descMap.attrInt == ATTRIBUTE_SYSTEM_MODE) {
log.debug "MODE - ${descMap.value}"
Expand All @@ -282,6 +265,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,9 +301,17 @@ 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
}
map = validateOperatingStateBugfix(map)
// Check to see if this was changed, if so make sure we have the correct heating setpoint
if (map.data?.correctedValue) {
sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT))
}
}
}
}
Expand All @@ -331,6 +324,108 @@ 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)
}
state.rawTemp = intVal
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")
}

handleOperatingStateBugfix()
}

map
}

// Due to a bug in this model's firmware, sometimes we don't get
// an updated operating state; so we need some special logic to verify the accuracy.
// TODO: Add firmware version check when change versions are known
// The logic between these two functions works as follows:
// In temperature and heatingSetpoint events check to see if we might need to request
// the current operating state and request it with handleOperatingStateBugfix.
//
// In operatingState events validate the data we received from the thermostat with
// the current environment, adjust as needed. If we had to make an adjustment, then ask
// for the setpoint again just to make sure we didn't miss data somewhere.
//
// There is a risk of false positives where we receive a new valid operating state before the
// new setpoint, so we basically toss it. When we come to receiving the setpoint or temperature
// (temperature roughly every minute) then we should catch the problem and request an update.
// I think this is a little easier than outright managing the operating state ourselves.
// All comparisons are made using the raw integer from the thermostat (unrounded Celsius decimal * 100)
// that is stored in temperature and setpoint events.

/**
* Check if we should request the operating state, and request it if so
*/
def handleOperatingStateBugfix() {
def currOpState = device.currentValue("thermostatOperatingState")

if (state.rawSetpoint != null && state.rawTemp != null) {
if (state.rawSetpoint <= state.rawTemp) {
if (currOpState != "idle")
sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE))
} else {
if (currOpState != "heating")
sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE))
}
}
}
/**
* Given an operating state event, check its validity against the current environment
* @param map An operating state to validate
* @return The passed map if valid, or a corrected map and a new param data.correctedValue if invalid
*/
def validateOperatingStateBugfix(map) {
// If we don't have historical data, we will take the value we get,
// otherwise validate if the difference is > 1
if (state.rawSetpoint != null && state.rawTemp != null) {
def oldVal = map.value

if (state.rawSetpoint <= state.rawTemp) {
map.value = "idle"
} else {
map.value = "heating"
}

// Indicate that we have made a change
if (map.value != oldVal) {
map.data = [correctedValue: true]
}
}
map
}

def updateWeather() {
log.debug "updating weather"
def weather
Expand Down Expand Up @@ -387,14 +482,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 @@ -516,10 +621,10 @@ def configure() {
requests += zigbee.addBinding(THERMOSTAT_CLUSTER)
// Configure Thermostat Cluster
requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMP, DataType.INT16, 10, 60, 50)
requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT, DataType.INT16, 1, 0, 50)
requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT, DataType.INT16, 1, 600, 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, 600, 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 +675,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)
}