I am new to Groovy coding and I am trying to learn how to do apps. I have done some successful atempts on various apps and I now want to understand maps and how to use them.
To understand maps I want to do a dynamic entry into a log file. I have functionality that calculates a temperature scenario number depending on the outside temperature. This bit of the code works fine and presents the scenario number in the log file (1-6). To ad a description to this number I want to learn how to use maps. So I did the following:
In the begining of the script I declared an empty script global map so that all methods should have access to it. This is before the definition method in the app script:
In the initialize method I then add values to the empty map. The numbers 1-6 represents the scenario value to use to select the corresponding description. The map should by now exist and be filled with values.
// Dynamic Scenario Description map
temperatureScenarioDescriptions = [
1: "Temperature below -10°",
2: "Temperature between -10° and 0°",
3: "Temperature between 0° and 10°",
4: "Temperature between 10° and 20°",
5: "Temperature between 20° and 30°",
6: "Temperature over 30°"
]
The method where I am doing the dynamic log description looks like this. Other methods calculates the scenario numbers and then they are transferred to this method.
def updateScenario() {
def temperatureScenario = state.temperature ?: "No value"
def light = state.light ?: "No value"
def rain = state.rain ?: "No value"
def humidity = state.humidity ?: "No value"
// Access the global field to get the description
def temperatureScenarioDescription = temperatureScenarioDescriptions[temperatureScenario] ?: "Unknown Scenario"
log.info "Update Scenario: Current Temperature Scenario: ${temperatureScenario} - ${temperatureScenarioDescription}"
log.info "Update Scenario: Current Light Scenario: ${light}"
log.info "Update Scenario: Current Rain Scenario: ${rain}"
log.info "Update Scenario: Current Humidity Scenario: ${humidity}"
// Notify child apps
childApps.each { child ->
child.updateScenario(
state.temperature ?: 0,
state.light ?: 0,
state.rain ?: 0,
state.humidity ?: 0
)
}
}
(The last bits about the child app is under development and (I don't think) relevant to the issue at hand.)
The log file turns out: 'Update Scenario: Current Temperature Scenario: 5 - Unknown Scenario'
What I want the log file to say: 'Update Scenario: Current Temperature Scenario: 5 - Temperature between 20° and 30°'
Why is not the correct value fetched/found for the description?
Log file:
app:8552024-07-19 13:40:44.528infoUpdate Scenario: Current Humidity Scenario: 2
app:8552024-07-19 13:40:44.527infoUpdate Scenario: Current Rain Scenario: 1
app:8552024-07-19 13:40:44.526infoUpdate Scenario: Current Light Scenario: 2
app:8552024-07-19 13:40:44.525infoUpdate Scenario: Current Temperature Scenario: 5 - Unknown Scenario
How is temperatureScenario getting set here? Are you sure the data types match? Your Map keys should be integers (Groovy normally makes these strings unless they're unambiguously numeric, assuming yours are), but I don't think there's enough context to say what's happening for you.
You can log the output of a call like getObjectClassName(temperatureScenario) to see for sure--same with any object.
No, I am not. This one of the things that I have just recently discovered, that I need to keep track of the datatypes throughout the script. I have a lot of input from events, I do calculations and use a veriaty of variables. I have added a lot of .toDouble() and .toInteger() all over the script to try and mitigate this, but in all fairness, a bit arbitrary as I had so far missed the getObjectClassName() feature. Thanks for pointing me towards that. I think I need to start there and go through the script again to verify that the data types are not the culprit.
I should add that the above is more or less a guess, but if you're looking up something in a Map that you think is there but aren't finding with what is seemingly the correct key, that's a good place to start looking.
Ah, thanks, I didn't know how to get the object type so that was imensly helpful. I realy needed to have a look at that as I had forgotten what can and cannot be converted to what and why. Now I am resonably sure all values concerned are integers.
Unfortunately the description from the map still comes out as the default if not found, 'Unknown Scenario'. So if you have som time over and feel the need to have something to ponder with the morning coffee, all thoughts are appreciated.
You mentioned context so I decided to post the full script for reference. The main method where I try to get the description is in updateScenario() so that you can search for it and find it easy should you which to do so. I also use it in getTemperatureScenario() but I have left that one as is while I try to get tor former to work.
You will find that there is some repeats in the code where I try different things to get it to work.
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- House heating and cooling - Parent App -------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
// - The purpose of this app is to collect local outdoor weather information from sensors, then based on intervals the values fall
// - within, determine unique weather scenarios and pass this information on to child apps governing the heating and cooling of the
// - house. In essence, it is a meteorological app that provides weather information. The app communicates with four sensor types:
//
// Temperature sensors
// Light sensors
// Rain sensors
// Humidity sensors
//
// - Besides the sensor information, the app calculates average day temperatures and based on those, calculates the current
// - meteorological season.
//
// The child apps governing the heating and cooling of the house are:
//
// Air-to-air heat pump handler app
// Underfloor heating handler app
// Radiator handler app
// The roof fan handler app
//
// - Each child app works independently of each other and has the option to act on the common weather information provided by this
// - app.
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Version handling -----------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
//
// 2024-07-15 Version 0.1 created.
//
// 2024-07-16 Version 0.2 Updated to improve value conversions and scenario calculations
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Map declarations -----------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
import groovy.transform.Field
@Field def temperatureScenarioDescriptions = [:]
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Definition -----------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
definition(
name: "App - House heating and cooling - Parent App",
namespace: "magnus_s",
author: "magnus_s",
description: "Evaluates weather scenarios based on sensor readings",
category: "Convenience",
iconUrl: "",
iconX2Url: "",
iconX3Url: ""
)
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Preferences ----------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
preferences {
page(name: "mainPage", title: "<b>Setup devices, time restrictions and temperature levels</b>", install: true, uninstall: true) {
section("<b>Ideal indoor temperature</b>", hideable: true, hidden: false) {
paragraph "<b>Set the temperature you would like the system to aim for</b>"
input "indoorTargetTemperatureV", "decimal", title: "Set Value for desired indoor temperature (C)", description: "Enter a Value for desired indoor temperature (C)", required: true, defaultValue: fetchHubVariable("inside_target_temperature_gv")
}
section("<b>Temperature devices and temperature calculation defaults</b>", hideable: true, hidden: true) {
paragraph "<b>Select temperature devices</b>"
input "indoorTemperatureDevice", "capability.temperatureMeasurement", title: "Select indoor thermometer device", required: true, multiple: false
input "outdoorTemperatureDevice", "capability.temperatureMeasurement", title: "Select outdoor thermometer device", required: true, multiple: false
paragraph "<b>Enter default values for temperature threshold calculations</b>"
input "indoorTempStartHeating_deltaV", "decimal", title: "At how much lower temperature than the ideal temperature would you like the heating to start heating the house? (C)", description: "Enter a Value for when to start heating (C)", required: true, defaultValue: fetchHubVariable("inside_temp_heating_point_gv")
input "indoorTempStartCooling_deltaV", "decimal", title: "At how much higher temperature than the ideal temperature would you like to start cooling the house? (C)", description: "Enter a Value for when to start heating (C)", required: true, defaultValue: fetchHubVariable("inside_temp_cooling_point_gv")
paragraph "<b>Enter interval values for temperature scenarios</b>"
input "outdoorTempScenario_To1_V", "decimal", title: "Enter an open-ended TO temperature value for scenario 1 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: -10
input "outdoorTempScenario_From2_V", "decimal", title: "Enter a FROM temperature value for scenario 2 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: -10
input "outdoorTempScenario_To2_V", "decimal", title: "Enter a TO temperature value for scenario 2 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 0
input "outdoorTempScenario_From3_V", "decimal", title: "Enter a FROM temperature value for scenario 3 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 0
input "outdoorTempScenario_To3_V", "decimal", title: "Enter a TO temperature value for scenario 3 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 10
input "outdoorTempScenario_From4_V", "decimal", title: "Enter a FROM temperature value for scenario 4 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 10
input "outdoorTempScenario_To4_V", "decimal", title: "Enter a TO temperature value for scenario 4 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 20
input "outdoorTempScenario_From5_V", "decimal", title: "Enter a FROM temperature value for scenario 5 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 20
input "outdoorTempScenario_To5_V", "decimal", title: "Enter a TO temperature value for scenario 5 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 30
input "outdoorTempScenario_From6_V", "decimal", title: "Enter an open-ended FROM temperature value for scenario 6 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 30
}
section("<b>Sunlight device and threshold values for sunlight</b>", hideable: true, hidden: false) {
paragraph "<b>Select outdoor illumination measurement device</b>"
input "SunlightDevice", "capability.illuminanceMeasurement", title: "Select a UV measurement device", required: true, multiple: false
paragraph "<b>Set ranges for the light sensor</b>"
input "settingOutdoorSunLight_Sunny_v", "decimal", title: "Set threshold sunlight value for sunny", description: "Set threshold value for sunny", required: true, defaultValue: 4000
input "settingOutdoorSunLight_Dusk_v", "decimal", title: "Set threshold sunlight value for dusk", description: "Set threshold value for dusk", required: true, defaultValue: 400
}
section("<b>Rain meter device</b>", hideable: true, hidden: true) {
paragraph "<b>Select devices for rain measurement</b>"
input "rainDevice", "capability.sensor", title: "Select a rainmeter device", required: true, multiple: false
}
section("<b>Outdoor humidity device</b>", hideable: true, hidden: true) {
paragraph "<b>Select devices for humidity measurement</b>"
input "humidityDevice", "capability.relativeHumidityMeasurement", title: "Select an outdoor humidity device", required: true, multiple: false
}
// section("Child Apps") {
// app(name: "childApps", appName: "Child Weather App", namespace: "yourNamespace", title: "Add Child Weather App", multiple: true)
// }
}
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Installation and Initialization --------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
def installed() {
log.info "Installed"
initialize()
}
def updated() {
log.info "Updated"
unsubscribe()
initialize()
}
def initialize() {
log.info "Initialized"
// Get initial values
getInitialValues()
// Schedule season evaluation daily
schedule("0 0 0 * * ?", season_handler) // Run season_handler daily at midnight
// Subscribe to hub variables
subscribe(location, "inside_target_temperature_gv", fetchHubVariables)
subscribe(location, "rain_gv", fetchHubVariables) // remove?
subscribe(location, "rain_Sum_Hour_gv", fetchHubVariables) // remove?
subscribe(location, "rain_Sum_Day_gv", fetchHubVariables) // remove?
subscribe(location, "average_temp_last_24h_gv", fetchHubVariables)
// Subscribe to events
subscribe(indoorTemperatureDevice, "temperature", indoorTemperatureHandler)
subscribe(outdoorTemperatureDevice, "temperature", outdoorTemperatureHandler)
subscribe(SunlightDevice, "illuminance", outdoorLightHandler)
subscribe(humidityDevice, "humidity", outdoorHumidityHandler)
subscribe(rainDevice, "rainUnits", rainHandler_rain)
subscribe(rainDevice, "rainSumHourUnits", rainHandler_rainSumHour)
subscribe(rainDevice, "rainSumDayUnits", rainHandler_rainSumDay)
// Get the hub variables
fetchHubVariables()
// Calculate variables and update the equivalent hub variables with new values
calculateVariables()
// Other initialization actions
passSettingsToChildren()
// Dynamic Scenario Description map
temperatureScenarioDescriptions = [
1: "Temperature below -10°",
2: "Temperature between -10° and 0°",
3: "Temperature between 0° and 10°",
4: "Temperature between 10° and 20°",
5: "Temperature between 20° and 30°",
6: "Temperature over 30°"
]
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Helper handler -------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Convert variables safely handler -------------------------
def toDoubleSafe(value, defaultValue = 0.00) {
try {
return value?.toDouble() ?: defaultValue
} catch (Exception e) {
log.warn "Conversion to Double failed for value: ${value}"
return defaultValue
}
}
// Helper method to safely convert to BigInteger
def toBigIntegerSafe(value) {
try {
return value?.toBigInteger() ?: BigInteger.ZERO
} catch (Exception e) {
log.warn "Conversion to BigInteger failed for value: ${value}"
return BigInteger.ZERO
}
}
// Helper method to safely convert to Integer
def toIntSafe(value) {
try {
return value?.toInteger() ?: 0
} catch (Exception e) {
log.warn "Conversion to Integer failed for value: ${value}"
return 0
}
}
// ---------------------------- Extract teh numbers only from the rain evens with units of measure handler -------------------------
def extractNumber(value) {
def numberString = value.replaceAll("[^0-9.]", "")
return numberString as Double
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Capture initial values -----------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
// This method is to get the values that was lastly created by a sensor, to give the variables a current value, while waiting for the next update.
def getInitialValues() {
// Get current outdoor sensor values
def currentOutdoorTemperature_fetchedV = outdoorTemperatureDevice.currentValue("temperature")
state.currentOutdoorTemperatureV = toDoubleSafe(currentOutdoorTemperature_fetchedV)
// log.debug "Initial current temperature value of the temperatureDevice is ${currentOutdoorTemperature_fetchedV}, converted: ${state.currentOutdoorTemperatureV}"
def currentOutdoorLighting_fetchedV = SunlightDevice.currentValue("illuminance")
state.currentOutdoorLightV = toBigIntegerSafe(currentOutdoorLighting_fetchedV)
// log.debug "Initial current illumination value of the sunlightDevice is ${currentOutdoorLighting_fetchedV}, converted: ${state.currentOutdoorLightV}"
def currentOutdoorHumidityDevice_fetchedV = humidityDevice.currentValue("relativeHumidityMeasurement")
state.currentOutdoorHumidityV = toIntSafe(currentOutdoorHumidityDevice_fetchedV)
// log.debug "Initial current relativeHumidityMeasurement value of the humidityDevice is ${currentOutdoorHumidityDevice_fetchedV}, converted: ${state.currentOutdoorHumidityV}"
/*
// def currentOutdoorRain_fetchedV = rainDevice.currentValue("rainSumDay")
def currentRainSumHourV = fetchDeviceAttribute(rainDevice, "rainSumHour")
state.currentRainSumHourV = toDoubleSafe(currentRainSumHourV)
//state.currentRain_RainSumHourV = toDoubleSafe(currentOutdoorRain_fetchedV)
// log.debug "Initial current rain in the last hour value of the rainDevice is ${state.currentRainSumHourV}, converted: ${state.currentRainSumHourV}"
*/
updateScenario()
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Retrieve and Update Hub Variables ------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
// Method to fetch all hub variables and log them
def fetchHubVariables() {
// Fetching hub variables
def indoorTargetTemperatureV = fetchHubVariable("inside_target_temperature_gv")
def indoorTempStartHeatingV = fetchHubVariable("inside_temp_heating_point_gv")
def indoorTempStartCoolingV = fetchHubVariable("inside_temp_cooling_point_gv")
def averageOutsideTempV = fetchHubVariable("average_temp_last_24h_gv")
/*
def rainHubValiable_rainV = fetchHubVariable("rain_gv")
def rainHubValiable_rainSumHourV = fetchHubVariable("rain_Sum_Hour_gv")
def rainHubValiable_rainSumDayV = fetchHubVariable("rain_Sum_Day_gv")
log.debug "Rainvalues from hub variables as triggered: rain: ${rainHubValiable_rainV}"
log.debug "Rainvalues from hub variables as triggered: rainSumHour: ${rainHubValiable_rainSumHourV}"
log.debug "Rainvalues from hub variables as triggered: rainSumDay: ${rainHubValiable_rainSumDayV}"
// Fetching device attributes
def rainV = fetchDeviceAttribute(rainDevice, "rain")
state.rainV = toDoubleSafe(rainV)
def rainSumHourV = fetchDeviceAttribute(rainDevice, "rainSumHour")
state.rainSumHourV = toDoubleSafe(rainSumHourV)
def rainSumDayV = fetchDeviceAttribute(rainDevice, "rainSumDay")
state.rainSumDayV = toDoubleSafe(rainSumDayV)
*/
}
// Method to fetch the value of a hub variable and log it
def fetchHubVariable(varName) {
def variable = getGlobalVar(varName)
return variable?.value
}
/*
// Method to fetch the value of a device attribute and log it
def fetchDeviceAttribute(device, attributeName) {
def attributeValue = device?.currentValue(attributeName)
return toDoubleSafe(attributeValue)
}
*/
// Method to update the hub variable only if the new value is different
def checkAndUpdateHubVariable(varName, newValue) {
def currentValue = getGlobalVar(varName)?.value
if (currentValue != newValue) {
setGlobalVar(varName, newValue)
}
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- calculate Variables ------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
def calculateVariables() {
// Calculate at what temperature to start heating
indoorTempStartHeatingV = indoorTargetTemperatureV - indoorTempStartHeating_deltaV
state.indoorTempStartHeatingV = indoorTempStartHeatingV
setGlobalVar("inside_temp_heating_point_gv", state.indoorTempStartHeatingV)
// Calculate at what temperature to start cooling
indoorTempStartCoolingV = indoorTargetTemperatureV + indoorTempStartCooling_deltaV
state.indoorTempStartCoolingV = indoorTempStartCoolingV
setGlobalVar("inside_temp_cooling_point_gv", state.indoorTempStartCoolingV)
// Calculate at what temperature to stop heating
indoorTempStopHeatingV = indoorTargetTemperatureV
state.indoorTempStopHeatingV = indoorTempStopHeatingV
setGlobalVar("inside_temp_stop_heating_point_gv", state.indoorTempStopHeatingV)
// Calculate at what temperature to stop cooling
indoorTempStopCoolingV = indoorTargetTemperatureV
state.indoorTempStopCoolingV = indoorTempStopCoolingV
setGlobalVar("inside_temp_stop_cooling_point_gv", state.indoorTempStopCoolingV)
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Sensor Handlers ------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
//
// When a new sensor event occurs, the respective updateScenario variable is updated individually ----------------------------------
def indoorTemperatureHandler(evt) {
log.debug "Indoor temperature handler: New event received: ${evt.value} from ${evt.device}"
def currentIndoorTemperatureV = toDoubleSafe(evt.value)
state.currentIndoorTemperatureV = currentIndoorTemperatureV
// log.debug "Indoor temperature handler: Current average indoor temperature: ${state.currentIndoorTemperatureV}"
}
def outdoorTemperatureHandler(evt) {
log.debug "Outdoor temperature handler: New event received: ${evt.value} from ${evt.device}"
def currentOutdoorTemperatureV = toDoubleSafe(evt.value)
state.currentOutdoorTemperatureV = currentOutdoorTemperatureV
state.temperatureScenario = getTemperatureScenario(state.currentOutdoorTemperatureV)
log.debug "state.currentOutdoorTemperatureV ${state.currentOutdoorTemperatureV} was sent by the outdoorTemperatureHandler to getLightScenario() that returned state.temperatureScenario = ${state.temperatureScenario}"
log.debug "Outdoor temperature handler: Current average outside temperature: ${state.currentOutdoorTemperatureV}"
updateScenario()
}
def outdoorLightHandler(evt) {
log.debug "New outdoor illumination event recieved: ${evt.value} from ${evt.device}"
def currentOutdoorLightV = toIntSafe(evt.value)
state.currentOutdoorLightV = currentOutdoorLightV
// log.debug "parameter ${currentOutdoorLightV} was stored into state ${state.currentOutdoorLightV}"
state.lightScenario = getLightScenario(state.currentOutdoorLightV)
log.debug "state.currentOutdoorLightV ${state.currentOutdoorLightV} was sent byt the outdoorLightHandler to getLightScenario() that returned state.lightScenario = ${state.lightScenario}"
updateScenario()
}
def outdoorHumidityHandler(evt) {
log.debug "New outdoor humidity event occurred: ${evt.value} from ${evt.device}"
def currentOutdoorHumidityV = toIntSafe(evt.value)
state.currentOutdoorHumidityV = currentOutdoorHumidityV
state.humidityScenario = getHumidityScenario(state.currentOutdoorHumidityV)
log.debug "state.currentOutdoorHumidityV ${state.currentOutdoorHumidityV} was sent by the outdoorHumidityHandler to the getHumidityScenario() that returned state.humidityScenario = ${state.humidityScenario}"
updateScenario()
}
def rainHandler_rain(evt = null) {
// log.trace "rainHandler_rain()"
if (evt) {
// log.info "rainHandler_rain() called: ${evt.value} ${evt.device}"
def rainValue = evt.value
if (rainValue == null) {
log.warn "rainHandler_rain() received null value"
} else {
state.rainValue = extractNumber(rainValue)
log.debug "rainValue = ${state.rainValue}"
}
} else {
log.warn "rainHandler_rain() called with null event"
}
}
def rainHandler_rainSumHour(evt = null) {
// log.trace "rainHandler_rainSumHour()"
if (evt) {
// log.info "rainHandler_rainSumHour() called: ${evt.device} ${evt.value}"
def rainSumHourValue = evt.value
if (rainSumHourValue == null) {
log.warn "rainHandler_rainSumHour() received null value"
} else {
state.rainSumHourValue = extractNumber(rainSumHourValue)
log.debug "rainSumHourValue = ${state.rainSumHourValue}"
state.rainScenario = getRainScenario(state.rainSumHourValue)
log.debug "state.rainSumHourValue ${state.rainSumHourValue} was sent by the rainHandler_rainSumHour handler to getRainScenario() that returned state.rainScenario = ${state.rainScenario}"
}
} else {
log.warn "rainHandler_rainSumHour() called with null event"
}
}
def rainHandler_rainSumDay(evt = null) {
// log.trace "rainHandler_rainSumDay()"
if (evt) {
// log.info "rainHandler_rainSumDay() called: ${evt.device} ${evt.value}"
def rainSumDayValue = evt.value
if (rainSumDayValue == null) {
log.warn "rainHandler_rainSumDay() received null value"
} else {
state.rainSumDayValue = extractNumber(rainSumDayValue)
log.debug "rainSumDayValue = ${state.rainSumDayValue}"
}
} else {
log.warn "rainHandler_rainSumDay() called with null event"
}
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Scenario Selection Handlers -----------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
def getTemperatureScenario(temperature) {
def temperatureScenario
switch (temperature) {
case { it < outdoorTempScenario_To1_V }:
temperatureScenario = 1
break
case { it >= outdoorTempScenario_From2_V && it < outdoorTempScenario_To2_V }:
temperatureScenario = 2
break
case { it >= outdoorTempScenario_From3_V && it < outdoorTempScenario_To3_V }:
temperatureScenario = 3
break
case { it >= outdoorTempScenario_From4_V && it < outdoorTempScenario_To4_V }:
temperatureScenario = 4
break
case { it >= outdoorTempScenario_From5_V && it < outdoorTempScenario_To5_V }:
temperatureScenario = 5
break
case { it >= outdoorTempScenario_From6_V }:
temperatureScenario = 6
break
default:
log.warn "Unknown temperature value: ${temperature}"
temperatureScenario = 0
}
state.temperatureScenario = temperatureScenario
def temperatureScenarioDescription = temperatureScenarioDescriptions[temperatureScenario] ?: "Unknown scenario"
log.debug "Temperature scenario updated. Temperature now ${temperature} and falls into scenario ${temperatureScenario} - ${temperatureSenarioDescription}"
return temperatureScenario
}
def getLightScenario(light) {
def lightScenario
switch (light) {
case { it <= settingOutdoorSunLight_Dusk_v }:
lightScenario = 1
break
case { it > settingOutdoorSunLight_Dusk_v && it < settingOutdoorSunLight_Sunny_v }:
lightScenario = 2
break
case { it >= settingOutdoorSunLight_Sunny_v }:
lightScenario = 3
break
default:
log.warn "Unknown light value: ${light}"
lightScenario = 0
}
log.debug "Light scenario updated: Light now ${light} and falls into scenario ${lightScenario}"
return lightScenario
}
def getHumidityScenario(humidity) {
def humidityScenario
if (humidity < 40) {
humidityScenario = 1
} else if (humidity >= 40 && humidity < 60) {
humidityScenario = 2
} else if (humidity >= 60) {
humidityScenario = 3
} else {
log.warn "Unknown humidity value: ${humidity}"
humidityScenario = 0
}
log.debug "Humidity scenario updated. Humidity ${humidity} now falls into scenario ${humidityScenario}"
return humidityScenario
}
def getRainScenario(rain) {
def rainScenario
switch (rain) {
case { it < 0.1 }:
rainScenario = 1
break
case { it >= 0.1 }:
rainScenario = 2
break
default:
log.warn "Unknown rain condition: ${rain}"
rainScenario = 0
}
log.debug "Rain scenario updated. Rain ${rain} now falls into scenario ${rainScenario}"
return rainScenario
}
def updateScenario() {
def currentTemperatureScenario = state.temperatureScenario ?: "No value"
def currentLightScenario = state.lightScenario ?: "No value"
def currentRainScenario = state.rainScenario ?: "No value"
def currentHumidityScenario = state.humidityScenario ?: "No value"
log.debug "state.temperatureScenario = ${state.temperatureScenario}"
// Access the global field to get the description
def descriptionMapKey = toIntSafe(state.temperatureScenario)
log.debug "descriptionMapKey = ${descriptionMapKey}"
def temperatureScenarioDescription = temperatureScenarioDescriptions.get(descriptionMapKey, "Unknown Scenario")
log.debug "temperatureScenarioDescription in current scenatio = ${temperatureScenarioDescription}"
// def description5 = temperatureScenarioDescriptions.get(state.temperatureScenario, "Unknown Scenario")
def mapKeyObjectType = getObjectClassName(descriptionMapKey)
log.debug "descriptionMapKey Object Type = ${mapKeyObjectType}"
def temperatureScenarioObjectType = getObjectClassName(currentTemperatureScenario)
log.debug "currentTemperatureScenario Object Type = ${temperatureScenarioObjectType}"
def lightScenarioObjectType = getObjectClassName(currentLightScenario)
log.debug "currentLightScenario Object Type = ${lightScenarioObjectType}"
def rainScenarioObjectType = getObjectClassName(currentRainScenario)
log.debug "currentRainScenario Object Type = ${rainScenarioObjectType}"
def humidityScenarioObjectType = getObjectClassName(currentHumidityScenario)
log.debug "currentHumidityScenario Object Type = ${humidityScenarioObjectType}"
log.info "Latest updated Scenario: Current Temperature Scenario: ${currentTemperatureScenario} - ${temperatureScenarioDescription}"
log.info "Latest updated Scenario: Current Light Scenario: ${currentLightScenario}"
log.info "Latest updated Scenario: Current Rain Scenario: ${currentRainScenario}"
log.info "Latest updated Scenario: Current Humidity Scenario: ${currentHumidityScenario}"
// Notify child apps
childApps.each { child ->
child.updateScenario(
state.state.temperatureScenario ?: 0,
state.state.lightScenario ?: 0,
state.state.rainScenario ?: 0,
state.state.humidityScenario ?: 0
)
}
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Season Handlers ------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
// Handler for season evaluation
def season_handler() {
// Initialize state variables to keep track of consecutive days for each season
if (!state.tropicalSummerDays) state.tropicalSummerDays = 0
if (!state.summerDays) state.summerDays = 0
if (!state.autumnSpringDays) state.autumnSpringDays = 0
if (!state.winterDays) state.winterDays = 0
log.debug "Evaluating season with state.averageOutsideTempV: ${state.averageOutsideTempV}"
def avgTemp = state.averageOutsideTempV
def seasonChanged = false
if (avgTemp >= 20.0) {
state.tropicalSummerDays++
state.summerDays = 0
state.autumnSpringDays = 0
state.winterDays = 0
if (state.tropicalSummerDays >= 5) {
state.seasonV = "Tropical summer"
seasonChanged = true
}
} else if (avgTemp >= 10.0 && avgTemp < 20.0) {
state.summerDays++
state.tropicalSummerDays = 0
state.autumnSpringDays = 0
state.winterDays = 0
if (state.summerDays >= 5) {
state.seasonV = "Summer"
seasonChanged = true
}
} else if (avgTemp >= 0.0 && avgTemp < 10.0) {
state.autumnSpringDays++
state.tropicalSummerDays = 0
state.summerDays = 0
state.winterDays = 0
if (state.autumnSpringDays >= 5) {
state.seasonV = "Autumn/Spring"
seasonChanged = true
}
} else if (avgTemp < 0.0) {
state.winterDays++
state.tropicalSummerDays = 0
state.summerDays = 0
state.autumnSpringDays = 0
if (state.winterDays >= 5) {
state.seasonV = "Winter"
seasonChanged = true
}
}
if (seasonChanged) {
log.info "The current season is: ${state.seasonV}"
}
}
// ---------------------------------------------------------------------------------------------------------------------------------
// ---------------------------- Child App Communication Handlers -------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------------------------------------
def passSettingsToChildren() {
def settingsMap = [
tempSensor: settings.outdoorTemperatureDevice.id, // Corrected setting key to match input name
lightSensor: settings.SunlightDevice.id, // Corrected setting key to match input name
humiditySensor: settings.humidityDevice.id, // Corrected setting key to match input name
rainSensor: settings.rainDevice.id // Corrected setting key to match input name
]
childApps.each { child ->
child.receiveParentSettings(settingsMap)
}
}
def logFromChild(message) {
log.debug "Child App Log: ${message}"
}
And here are the latest log entries where the map description isn't found and instead defaulted to 'Unknown Scenario':
EDIT: I edited the log file and removed som URLs for ease of reading, but it turned all light green and wasn't to readble so I left it unpreformatted.
[app:855] 2024-07-19 23:25:17.512 Latest updated Scenario: Current Humidity Scenario: 3
[app:855] 2024-07-19 23:25:17.511 Latest updated Scenario: Current Rain Scenario: 1
[app:855] 2024-07-19 23:25:17.510 Latest updated Scenario: Current Light Scenario: 1
[app:855]2024-07-19 23:25:17.509 Latest updated Scenario: Current Temperature Scenario: 4 - Unknown Scenario
[app:855] 2024-07-19 23:25:17.507 currentHumidityScenario Object Type = java.lang.Integer
[app:855] 2024-07-19 23:25:17.506 currentRainScenario Object Type = java.lang.Integer
[app:855] 2024-07-19 23:25:17.505 currentLightScenario Object Type = java.lang.Integer
[app:855] 2024-07-19 23:25:17.503 currentTemperatureScenario Object Type = java.lang.Integer
[app:855] 2024-07-19 23:25:17.502 descriptionMapKey Object Type = java.lang.Integer
[app:855] 2024-07-19 23:25:17.501 temperatureScenarioDescription in current scenatio = Unknown Scenario
[app:855] 2024-07-19 23:25:17.500 descriptionMapKey = 4
[app:855] 2024-07-19 23:25:17.498 state.temperatureScenario = 4
[app:855] 2024-07-19 23:25:17.483 Initialized
[app:855] 2024-07-19 23:25:17.478 Updated
Looking at this a bit more closely, it seems that your field variable may need to be declared as static, otherwise it will be re-initialized (to effectively nothing) on every execution of your app. A static field will still be re-initialized on occasion (re-saving app code, rebooting the hub, etc.), but if you have a way to recover from that -- as you might with your initialize() method -- that should work.
But along those lines, note that initialize() in an app is not called unless you call it. You do when the user hits "Done" or "Install" (i.e., as part your updated() and installed() methods), but to account for the above, you'll also need to subscribe to reboots and run it then, too, something like subscribe(location, "systemStart", "rebootHandler") and call initialize() as part of your rebootHandler() method or similar.
Without running your code, this seems highly likely to be the case. If it's still not, I'd suggest paring your app down to a minimal example that demonstrates the problem; often, you'll discover what the problem is along the way yourself, but if not, it makes it easier for someone to reproduce without needing to understand the entire app (or set up devices to use with it).
Good morning Robert, I think your suggestion on the static declaration did the trick. When I stoped testing yesterday the descriptions didn't work, but this morning they do. At first I suspected WooDoo but then realised that I have two ways of getting the sensor values, a) if no event has occured yet, actively fetch the current value of the sensor and b) passivly wait for the next event and capture that. I suspect there is a bug somewhere in a) whereas b) now works after having declared the field variable as static. I am truly grateful for your assistance!
EDIT: I added state. to the maps when I assigned values to the maps. That sorted the a) scenario as well. Agan than you for you help! Very much appreciated!
If you want to persist data, state is normally the way to go. (This survived different executions of,the app, reboots, etc.). I assumed you had a reason for using a field variable, which is better as a sort of cache you can rebuild like I mentioned above (if you need to use it at all). Glad you got it figured out!
If you still are using both, note that they refer to entirely different objects, so I'd just stick to one, perhaps the one in state to make it simple.
Thanks Robert. Unfortunately it turned out that the hurras on my side were slightly premature.
I followed you recommendation and did a new app script containing only the code relevant to the issue at hand. A good thing is that the issue is consistant and repeats it self like clockwork, so easy to reproduce.
In a nutshell: If the script is initiated by a get current value method, the script works and produces a scenario number and dynamically depending on the scenario number, a corresponding description. If the script is initiated by an actual event, using the same code, the dynamic description comes out as 'Unknown description'. It is only the dynamic description that fails, the scenario number is genereated correctly.
The reason for having a get current value method is that the sensor can take up to an hour to create an event so the get current value method is needed for testing purposes, but otherwise not (This must be a common scenario which makes me fear the solution might be a very easy one that I have spend days on).
In the log file during the green bit, that is initiated by the get current value and the description works fine. Then when the script picks up events, the blue bit, the description is no longer found. This behavior, that one of the ways works but not the other, is consistant. Which part that works can vary I have noticed, but if one works, the other always fails.
Initially I suspected thay the issue was object type related as you suggested. I then checked all object types. Temperature readings are BigDecimal and scenario number is integer consistently as far as I can see. I then started to contemplate if there could be timing issues as the information could come from two ways? Was the (long) initialization (in the original script) really complete before the methods started to use the information that needded to be initialized? Is there a state issue somewhere? Again, I am sure I left out a comma somewhere with which it will work just fine, but I have at least tried all those things, and for instance making sure the get current values can only ever run once and that scenario numbers are left alone if they exist, which the do as state objects. The result is always the same so It looks like I have been barking up the wrong tree.
This is the script I put together to test the issue. The issue is consistant with the other script and repeatable. The script is runable but has Celsius degrees in the scenario selection.
I have a feeling I have overstayed my welcome and I don't want to take up more of your time, but if you would have one last look I would be very grateful. Any thoughts?
// This app is for testing purposes of the map description
definition(
name: "App - House heating and cooling - Test app for map descriptions",
namespace: "magnus_s",
author: "magnus_s",
description: "Evaluates weather scenarios based on sensor readings",
category: "Convenience",
iconUrl: "",
iconX2Url: "",
iconX3Url: ""
)
preferences {
page(name: "mainPage", title: "<b>Setup devices, time restrictions and temperature levels</b>", install: true, uninstall: true) {
section("<b>Select devices</b>", hideable: true, hidden: true) {
paragraph "<b>Select devices</b>"
input "outdoorTemperatureDevice", "capability.temperatureMeasurement", title: "Select outdoor thermometer device", required: true, multiple: false
paragraph "<b>Enter interval values for temperature scenarios</b>"
input "outdoorTempScenario_To1_V", "decimal", title: "Enter an open-ended TO temperature value for scenario 1 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: -10
input "outdoorTempScenario_From2_V", "decimal", title: "Enter a FROM temperature value for scenario 2 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: -10
input "outdoorTempScenario_To2_V", "decimal", title: "Enter a TO temperature value for scenario 2 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 0
input "outdoorTempScenario_From3_V", "decimal", title: "Enter a FROM temperature value for scenario 3 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 0
input "outdoorTempScenario_To3_V", "decimal", title: "Enter a TO temperature value for scenario 3 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 10
input "outdoorTempScenario_From4_V", "decimal", title: "Enter a FROM temperature value for scenario 4 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 10
input "outdoorTempScenario_To4_V", "decimal", title: "Enter a TO temperature value for scenario 4 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 20
input "outdoorTempScenario_From5_V", "decimal", title: "Enter a FROM temperature value for scenario 5 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 20
input "outdoorTempScenario_To5_V", "decimal", title: "Enter a TO temperature value for scenario 5 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 30
input "outdoorTempScenario_From6_V", "decimal", title: "Enter an open-ended FROM temperature value for scenario 6 (C)", description: "Enter a temperature value (C)", required: true, defaultValue: 30
}
}
}
def installed() {
log.info "Installed"
initialize()
}
def updated() {
log.info " "
log.info "Updated"
unsubscribe()
initialize()
}
def initialize() {
log.info "Initialized"
// Dynamic temperature scenario description map
state.temperatureScenarioDescriptions = [
1: "Temperature below -10°",
2: "Temperature between -10° and 0°",
3: "Temperature between 0° and 10°",
4: "Temperature between 10° and 20°",
5: "Temperature between 20° and 30°",
6: "Temperature over 30°"
]
// Subscribe to events
subscribe(outdoorTemperatureDevice, "temperature", outdoorTemperatureHandler)
simulateTemperatureEvent()
}
def simulateTemperatureEvent() {
log.debug "A temperature event was simulated"
def currentTemperature = getCurrentTemperatureValueFromDevice()
def fakeEvent = [value: currentTemperature, device: outdoorTemperatureDevice]
outdoorTemperatureHandler(fakeEvent)
}
def getCurrentTemperatureValueFromDevice() {
def currentFetchedTemperature = outdoorTemperatureDevice.currentValue("temperature")
log.debug "getCurrentTemperatureValueFromDevice returned ${currentFetchedTemperature}"
return currentFetchedTemperature
}
def outdoorTemperatureHandler(evt) {
log.debug "A new temperature event was received: ${evt.value} from ${evt.device}"
def currentOutdoorTemperature = evt.value.toBigDecimal()
def currentTemperatureScenario = getTemperatureScenario(currentOutdoorTemperature)
log.debug "getTemperatureScenario() was called and returned ${currentTemperatureScenario}"
def currentTemperatureScenarioDescription = getTemperatureScenarioDescription(currentTemperatureScenario)
log.debug "getTemperatureScenarioDescription() was called and returned ${currentTemperatureScenarioDescription}"
updateScenario(currentTemperatureScenario, currentTemperatureScenarioDescription)
}
def getTemperatureScenario(temperature) {
def temperatureScenario
switch (temperature) {
case { it < outdoorTempScenario_To1_V }:
temperatureScenario = 1
break
case { it >= outdoorTempScenario_From2_V && it < outdoorTempScenario_To2_V }:
temperatureScenario = 2
break
case { it >= outdoorTempScenario_From3_V && it < outdoorTempScenario_To3_V }:
temperatureScenario = 3
break
case { it >= outdoorTempScenario_From4_V && it < outdoorTempScenario_To4_V }:
temperatureScenario = 4
break
case { it >= outdoorTempScenario_From5_V && it < outdoorTempScenario_To5_V }:
temperatureScenario = 5
break
case { it >= outdoorTempScenario_From6_V }:
temperatureScenario = 6
break
default:
log.warn "Unknown temperature value: ${temperature}"
temperatureScenario = 0
}
return temperatureScenario
}
def getTemperatureScenarioDescription(temperatureScenario) {
def temperatureScenarioDescription = state.temperatureScenarioDescriptions.get(temperatureScenario, "Unknown Scenario")
return temperatureScenarioDescription
}
def updateScenario(currentTemperatureScenario, currentTemperatureScenarioDescription) {
log.info "currentTemperatureScenario = ${currentTemperatureScenario}"
log.info "currentTemperatureScenarioDescription = ${currentTemperatureScenarioDescription}"
}
I think what's going on for you is still ultimately an issue of type.
Looking into this more: you're storing a Map with dynamic types in state. It's possible "static" typing would solve this problem (I didn't test and don't recall off the top of my head...), but in any case, state gets serialized to/from JSON on every execution of your app, and what appears to be happening the second execution is that your numbers are, in fact, strings (reconstructed from this JSON). Try adding a line like this:
def getTemperatureScenarioDescription(temperatureScenario) {
// Add this line to see for yourself:
log.trace "key type = ${getObjectClassName(state.temperatureScenarioDescriptions.keySet().first())}"
def temperatureScenarioDescription = state.temperatureScenarioDescriptions.get(temperatureScenario, "Unknown Scenario")
return temperatureScenarioDescription
}
It's possible using a Map<Integer,String> would solve this problem, but an easy way to make it work for you in all cases is to either:
make the keys explicitly strings in the first place, and convert the value to string before lookup; or
parse the strings into an integer before comparison (probably more work than it's worth, IMHO)
You brilliant man! Thank you, I am no where near on my groovy journey yet to have figured that out (Especially since I did check and got integer). I opted for the solution to use strings consistently and convert when needed, and I almost dare not say it, but it seems to be working.
Many thanks Robert! I learned a ton! I am really grateful!