[2.3.2.134] RM not being triggered by one of a choice of Custom Attribute

@bravenel, I have a rule that is supposed to trigger on a custom attribute (Mix), but triggers on the wrong one (Rain).

Rule:

Trigger details and options:

Log:

The logs show that the event type that triggers it is “rain” when it should only have been triggered by “Mix”.

I will look into it.

1 Like

Please show me the Event Subscriptions from the App Status page.

1 Like

Thanks! Here you go:

Well, that should only fire the rule with the event value of "mix". Using a different device attribute but similar, I can't get it to fail. This is hub code, based on that subscription. Please try it again, and post logs again.

Once RM is woken up by the event, it's not going to check anything. This the equivalent of subscribing to "switch.on". Pretty certain that works reliably.

Did you by chance happen to hit Update or Done since the above failure?

The reason I ask is that this all changed in 2.3.2. If this was a pre-existing rule, it's subscription would have been to "precipitationType". And that would indeed have fired on any event. Hitting Done since 2.3.2 would have changed the subscription to what you showed above. I forgot to mention in the release notes the need to hit Done for existing rules with Custom Attribute triggers.

1 Like

That’s good to know!

I only got the Weatherflow Tempest a couple of weeks ago, so the rule is new since 2.3.2. I did click “Done” a few times, including after adding the logging bit to help me debug the issue.

I have another rule that is set to trigger on “Hail” and that one is working fine (Not triggering on neither Rain or Mix). Only triggering on “Mix” seems to be an issue. The 3 rules were all copies from each other.

I’m using the same driver and device as @bobbyD and @rlithgow1. Tagging them in case they are interested in trying to reproduce…

I’ll see if I can export the rule and include it here.

OK, what I don't know is what the subscription was at the time of the logs posted above. Now, if you can get it to fail with the current subscription, that would be quite the discovery.

If you have the source code for the driver, you could add a command to force it to send a 'mix' event. Or more to the point, to send a 'rain' event, to see if that trips the trigger.

1 Like

I will take a look to see if I can figure something out!

I do have the source code for the driver, but not sure where I would need to start unfortunately… I haven’t gotten to learn Groovy yet - only ever coded in Visual Basic, maybe 25 years ago…

Post the source here, or point me to it, and I will add the command. It's only a few lines of code, and then you can do it from the device page. Haha: new command "makeRain".

1 Like

This is the driver I’ve been using:

https://raw.githubusercontent.com/augoisms/hubitat/master/weatherflow/weatherflow.driver.groovy

This is the rule

{"deviceReplacements":{"7423":{"deviceName":"WeatherFlow Lite on Hub","deviceLabel":null,"deviceTypeName":"WeatherFlow Lite","deviceTypeNamespace":"hubitat"},"134":{"deviceName":"Bass","deviceLabel":"S\u00e9bastien\u2019s iPhone","deviceTypeName":"Mobile App Device","deviceTypeNamespace":"hubitat"},"4380":{"deviceName":"Echo - Solarium Echo Show 5 on Hub","deviceLabel":null,"deviceTypeName":"Echo Speaks Device","deviceTypeNamespace":"hubitat"}},"appReplacements":{"7600":{"appTypeName":"Rule-5.1","appTypeNamespace":"hubitat","appType":"sys","appName":"Rule-5.1","appLabel":"Notification - Tempest detected Rain and Hail \ud83c\udf27","parentAppInstalledAppId":"5009","parentAppTypeName":"Rule Machine","parentAppTypeNamespace":"hubitat","parentAppName":"Rule Machine","parentAppLabel":"Rule Machine","childApps":{},"singleInstance":false}},"appData":{"7600":{"state":{"parens":{"0":0},"actionDone":true,"ndx":8,"dateFormat":"yyyy-MM-dd","nestedBlock":[],"installedRules":[{"6510":"#TEST"},{"5071":"Alarm - Activate Blue-Red Colors in Den"},{"5205":"Alarm - Alert if in HSM Delay"},{"7086":"Alarm - Alert if water in Kitchen water filter bucket"},{"6543":"Alarm - Arm & Disarm HSM with Ring"},{"5142":"Alarm - Arm Ring"},{"7239":"Alarm - Arm Ring Away when everyone has left or mode becomes Away"},{"5207":"Alarm - Arm Ring Away with HSM Away"},{"5203":"Alarm - Arm Ring Home on the hour between 23:00 and 4:00 or with HSM/Mode"},{"7150":"Alarm - Arm after the required conditions have been met if failed the first time"},{"5050":"Alarm - Check that all doors are locked and send notification if not"},{"5206":"Alarm - Disarm Ring when Family Members arrive"},{"7278":"Alarm - Disarm in the morning"},{"5282":"Alarm - Lock All Doors and Close Garage with Arming HSM or Ring"},{"7238":"Alarm - Raise Den Shade on Alert"},{"7237":"Alarm - Raise or Lower Den Shade based on Alarm Status"},{"7151":"Alarm - Unlock doors with fire alarm"},{"7662":"Automation - Change mode back to day"},{"5655":"Automation - Den Aquarium Light Scheduler"},{"5070":"Automation - Excessive Blink (Front) Motion Events"},{"5815":"Automation - Fix Lights turned on by Shark IQ Robot"},{"6233":"Automation - Keep Garage Door Open if CO level triggered"},{"5506":"Automation - Lock front door when no motion in hallway for 15 minutes"},{"7235":"Automation - Lock the doors during the day at specific times once they are closed"},{"6430":"Automation - Lock the doors during the night once they are closed"},{"7310":"Automation - Master Bedroom Ceiling Fan"},{"7311":"Automation - Master Bedroom Fan - Set to Auto when the temperature is high and motion in evening"},{"5052":"Automation - Red Alert!"},{"6200":"Automation - Set Family Room Button 5 color based on Automatic Lights Disabled - Family Room"},{"5690":"Automation - Set the Dining Room Light to 20 when turned on"},{"7471":"Automation - Turn on #Manual Light Simulator"},{"6738":"Automation - Turn on Den Light Strips with motion and off without"},{"7246":"Automation - Turn on Den Light Strips with motion, off without"},{"5281":"Automation - Turn on lights for Downstairs Shark IQ"},{"6710":"Automation - Turn on the Master Bedroom Nightlight with Motion at night"},{"6708":"Automation - Turn on/off Basement Game Room Thermostat Display with motion"},{"6740":"Automation - Turn on/off Den Thermostat Display with motion"},{"6742":"Automation - Turn on/off Dimitri Thermostat Display with motion"},{"6744":"Automation - Turn on/off Dining Room Thermostat Display with motion"},{"6746":"Automation - Turn on/off Downstairs Bathroom Thermostat Display with motion"},{"6748":"Automation - Turn on/off Guest Room Thermostat Display with motion"},{"6750":"Automation - Turn on/off Kitchen Entrance Thermostat Display with motion"},{"6752":"Automation - Turn on/off Master Bedroom Thermostat Display with motion"},{"6328":"Automation - Turn on/off Xavier\u2019s Thermostat Display with motion"},{"5658":"Automation - Update outsideTemperature Variable with temperature changes"},{"6425":"Automation - Zigbee Offline"},{"6429":"Automation - Zigbee Online"},{"5561":"Automation - \ud83c\udf83 Halloween - Random Light - Outside Front Porch Light"},{"7439":"Automation - \ud83c\udf84 Refresh Outside Front Decoration Plug (Zooz)"},{"5300":"Automation - \ud83d\ude98 Opened Garage Door for Manon"},{"5298":"Automation - \ud83d\ude98 Opened Garage Door for S\u00e9bastien"},{"5302":"Automation - \ud83d\ude98 Opened Garage Door for Xavier"},{"6736":"Automation - \ud83d\ude98 Set Garage Door Security Light Color depending on door state"},{"5918":"Automation - \ud83d\ude98 Set Garage Door Security Light to off after it was set to green for 2 minutes"},{"5253":"Automation - \ud83d\ude98 Close garage door on Away Mode"},{"5296":"Automation - \ud83d\ude98 Opened Garage Door for Dimitri"},{"7695":"Control - Den Bookcase Motion (Restricted) follow Deb Bookcase Motion when contact is open only"},{"7240":"Control - Disable #Kasandra\u2019s Virtual Presence when #Kasandra-iPhone IP is not present for more than 12 hours"},{"7242":"Control - Enable #Kasandra\u2019s Virtual Presence when #Kasandra-iPhone IP is detected"},{"6895":"Control - Inovelli RGB LED Bar"},{"5141":"Control - Lock all doors"},{"7631":"Control - Prevent Kitchen Counter Light from turning on"},{"7119":"Control - Reboot hub when memory is low"},{"5367":"Control - Refresh Dining Room Shark IQ Robot in early morning"},{"7118":"Control - Solarium Fan speed based on temperature difference"},{"6756":"Control - Synch Family Room Picture Frame button 3 LED with Main Garage Door and Air Exchanger states"},{"6758":"Control - Synch Family Room Picture Frame button 4 LED with Basement Lights state"},{"6760":"Control - Synch Sebastien Closet and Family Room Picture Frame button 1 LED with Mode"},{"6762":"Control - Synch Sebastien Closet and Family Room Picture Frame button 2 LED with HSM"},{"6764":"Control - Synch Sebastien Closet button 3 LED with Master Bathroom Window state"},{"6766":"Control - Synch Sebastien Closet button 4 LED with Master Bathroom Lights State"},{"6574":"Control - Turn off Den Book Case Light when no motion"},{"6960":"Control - Turn off Master Bathroom Motion Stopped notification switch"},{"5304":"Control - Turn on power to Dry the Family Room Solar Dehumidifier"},{"6072":"Control - Turn on/off Lytmi with Harmony Hub Status"},{"5139":"Control - Unlock Front Door"},{"6936":"Control - \u2600\ufe0f 12v Outlet 1 turns off at 12.1 volts when Basement Outlet 2 is off"},{"6926":"Control - \u2600\ufe0f 12v Outlet 1 turns on at 12.6 volts when Solarium 12v Outlet is on"},{"6938":"Control - \u2600\ufe0f 12v Outlet 2 turns off at 12.1 volts"},{"6930":"Control - \u2600\ufe0f 12v Outlet 2 turns on at 12.6 volts when Basement Outlet 1 is on"},{"6934":"Control - \u2600\ufe0f 12v Solarium Outlet turns off at 12.2 volts when Basement Outlet 1 is off"},{"6932":"Control - \u2600\ufe0f 12v Solarium Outlet turns on at 12.4 volts"},{"7502":"Control - \u2600\ufe0f Confirm solar battery power is not getting drained"},{"6508":"Control - \u2600\ufe0f Update the refresh rate of the 100w Solar"},{"6265":"Control - \u2600\ufe0fTurn off 300w Solar Power Inverter depending on Voltage and output power"},{"6941":"Control - \u2600\ufe0fTurn off 300w Solar Power Inverter depending on Voltage and output power clone"},{"6391":"Control - \u2600\ufe0fTurn off 300w solar power inverter if lower than 12.15 volts"},{"6393":"Control - \u2600\ufe0fTurn on 300w Solar Power Inverter depending on Voltage"},{"5854":"Control - \ud83c\udf84 Den Holiday Three Button"},{"6754":"Control - \ud83c\udf84 Holiday Lights - Inside"},{"5138":"Control - \ud83d\ude98 Garage Door - Close by Cloud end point"},{"5137":"Control - \ud83d\ude98 Garage Door - Open by Cloud end point"},{"6794":"Notification - AC Cooling"},{"5042":"Notification - AC Cooling - Off"},{"6802":"Notification - Backyard East Gate was open during Evening, Night or Away"},{"6804":"Notification - Backyard West Gate was open during Evening, Night or Away - ADJUST TO USE TEMPEST WHEN WINDS ARE HIGH"},{"6039":"Notification - ChargePoint Breaker is offline"},{"5624":"Notification - Garage Door Opening while Inside Garage Door is open"},{"6232":"Notification - High CO in garage"},{"5368":"Notification - Main Garage Door (MyQ)"},{"6896":"Notification - Master Bathroom Light or Window about to turn off"},{"5136":"Notification - Master Bedroom Ceiling Fan Speed Change "},{"5043":"Notification - Mode is Night or Away with key downstairs windows open"},{"7406":"Notification - Refrigerator or Freezer Temperature Below Threshold - Garage Refrigerator"},{"6263":"Notification - Smoke detected in Garage"},{"7602":"Notification - Tempest detected Hail \ud83c\udf27"},{"7598":"Notification - Tempest detected Rain \u2614\ufe0f"},{"6394":"Notification - Thermostat SetPoint Change"},{"7088":"Notification - Time for Manon to get up"},{"5335":"Notification - Update weather from Weather Canada"},{"6897":"Notification - Upstairs Hallway light about to turn off"},{"5911":"Notification - \u2600\ufe0f Basement 100W Battery High Voltage"},{"6424":"Notification - \u2600\ufe0f Basement 100W Battery Low Voltage"},{"5916":"Notification - \u2600\ufe0f Basement 300W Battery High Voltage"},{"7603":"Notification - \u2614\ufe0f Raining with Key Windows Open"},{"6796":"Notification - \ud83d\udc15 Activate #Charlie might escape"},{"6800":"Notification - \ud83d\udc15 Activate #Stellie might escape"},{"5496":"Notification - \ud83d\udc15 Back Deck door open when Stellie might escape"},{"5504":"Notification - \ud83d\udc15 Maxwell Gate Open for more than 2 minutes"},{"5505":"Notification - \ud83d\udc15 Stellie Might Escape - Turn lights red in Solarium"},{"5501":"Notification - \ud83d\udc15 Stellie might escape by East Gate"},{"6806":"Notification - \ud83d\udc15 Stellie might escape by Maxwell\u2019s Gate"},{"5499":"Notification - \ud83d\udc15 Stellie might escape by West Gate"},{"7702":"Notification - \ud83e\uddca Temperature Below Threshold - Basement Chest Freezer (0.0)"},{"7704":"Notification - \ud83e\uddca Temperature Below Threshold - Basement Upright Freezer (-8.2)"},{"7700":"Notification - \ud83e\uddca Temperature Below Threshold - Garage Freezer (-9.35)"},{"7698":"Notification - \ud83e\uddca Temperature Below Threshold - Garage Fridge (7.83)"},{"7706":"Notification - \ud83e\uddca Temperature Below Threshold - Kitchen Freezer (20.88)"},{"7708":"Notification - \ud83e\uddca Temperature Below Threshold - Kitchen Fridge Full-Convert Drawer (0.0)"},{"7710":"Notification - \ud83e\uddca Temperature Below Threshold - Kitchen Fridge Sensor (0.0)"},{"6295":"Sub - Inovelli RGB LED Bar - Generic RGB LED Setup"},{"5208":"Sub - Set Mode"}],"clonedName":"Notification - Tempest detected Rain and Hail \ud83c\udf27","tCustomAttrType1":"NUMBER","tCustomAttrType4":"ENUM","tCustomAttrType5":"ENUM","selectActionsParams":{"label":"Notification - Tempest detected Rain and Hail \ud83c\udf27"},"editCondIf":null,"waitDone":{".2":{"1":true}},"stopOnST":false,"usesTime":false,"nestedIf":[],"trigCustoms":[],"repeating":null,"capabDone":true,"hhmmss-1":"0:02:00","SSecs-1":0,"skipping":false,"lastEvtName":"WeatherFlow Lite on Hub","predCapabs":[],"locationBlocked":[],"pbFixed":true,"ruleNdx":1,"firstR":{"0":true},"actionList":["1","4","2","3"],"timeTriggers":[],"locVars":{},"allVarsO":["outsideTemperature"],"actLabelIndent":"","tCustomAttrType-1":"ENUM","varSettingsUpdated":true,"certainTimes":[],"allVarsS":[],"allVarsT":[],"prevState":{"PB":"true","7423":"rain"},"eval":{"0":[]},"lastEvtText":null,"capabstrue":{"5":"WeatherFlow Lite on Hub reports precipitationType(none) mix"},"installedCapabs":["CarbonDioxideMeasurement","Configuration","RelativeHumidityMeasurement","ColorMode","HealthCheck","Initialize","PowerSource","SpeechSynthesis","Momentary","DoubleTapableButton","Light","TamperAlert","Battery","RelaySwitch","CarbonMonoxideDetector","ColorControl","PresenceSensor","SignalStrength","SpeechRecognition","WindowShade","Polling","ChangeLevel","PowerMeter","Switch","IlluminanceMeasurement","Tone","Bulb","Sensor","EnergyMeter","WaterSensor","ThreeAxis","Variable","VoltageMeasurement","ContactSensor","Notification","AudioNotification","ReleasableButton","Refresh","SwitchLevel","HoldableButton","ColorTemperature","MotionSensor","DoorControl","Outlet","Telnet","UltravioletIndex","FanControl","Actuator","Thermostat","MusicPlayer","MediaTransport","AudioVolume","PressureMeasurement","PushableButton","GarageDoorControl","Lock","TemperatureMeasurement","AccelerationSensor","Alarm","SmokeDetector","Chime"],"nestedSkipAll":[],"lastEvtDate":"2022-07-02","nestedInIf":[],"actions":{"1":{"wait":null,"delay":"","modes":{},"method":"getMsg","indent":"","rule":null,"label":"Notify S\u00e9bastien\u2019s iPhone and Speak on Echo - Solarium Echo Show 5 on Hub: 'It has started hailing and raining'\n","cond":0},"2":{"wait":[1],"delay":"","modes":{},"method":"getWaitEvents","indent":"","rule":null,"label":"Wait for event: WeatherFlow Lite on Hub is precipitationType(none) none\n","cond":0},"3":{"wait":null,"delay":"","modes":{},"method":"getMsg","indent":"","rule":null,"label":"Notify S\u00e9bastien\u2019s iPhone and Speak on Echo - Solarium Echo Show 5 on Hub: 'It has stopped hailing and raining'\n","cond":0},"4":{"wait":null,"delay":"","modes":{},"method":"getLogMsg","indent":"","rule":null,"label":"Log: 'Event Trigger: Mix; Actual event: %value%(Event Trigger: Mix; Actual event: 0)'\n","cond":0}},"usesDate":false,"actNdx":6,"lastEvtValue":0,"allVarsGT":[],"installed":true,"waitEvents":[],"private":"true","waitNdx":{".2":[1]},"rCustomAttrType_7":"ENUM","rCustomAttrType_2":"ENUM","isPredicate":true,"allVars":{"outsideTemperature":{"sourceIp":"","attribute":null,"source":"","type":"Decimal","value":19.6,"deviceId":null}},"simpleCond":false,"changedValues":true,"nestedRepIf":[],"lastEvtTime":"15:45","condOper":"cond","nestedElse":[],"allVarsI":[],"varTriggersFixed":true,"waitMethod":"allHandlerW","needWaitCancel":false,"cutAction":[],"allVarsD":["outsideTemperature"],"broken":false,"stopped":false,"rulesList":[{"6701":"Family Room Picture Frame on Radios: button 1 doubleTapped"}],"nestedLabel":[],"oldInputAct":true,"waitElapsed":false,"inputAct":true,"lastEvtDevId":"7423","token":0,"waitCondNdx":2,"capabsfalse":{"7":"NOT WeatherFlow Lite on Hub precipitationType(none) mix"},"SHours-1":0,"stPrimed":true,"timeTriggersW":{},"allLocalVars":{},"varSettingsOld":{},"timeFormat":"HH:mm","olddValues":true,"needWaitDone":false,"SMins-1":2},"appSettings":[{"deviceList":null,"multiple":false,"name":"tCapab2","type":"enum","value":""},{"deviceList":null,"multiple":false,"name":"actionDone","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"tCapab5","type":"enum","value":"Custom Attribute"},{"deviceList":{"7423":"WeatherFlow Lite on Hub"},"multiple":false,"name":"tDev-1","type":"capability.","value":null},{"deviceList":null,"multiple":false,"name":"tCapab6","type":"enum","value":""},{"deviceList":null,"multiple":true,"name":"copyAct","type":"enum","value":null},{"deviceList":null,"multiple":false,"name":"logmsg.4","type":"textarea","value":"Event Trigger: Mix; Actual event: %value%"},{"deviceList":null,"multiple":false,"name":"pausRule","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"tCapab7","type":"enum","value":""},{"deviceList":null,"multiple":true,"name":"disableAct","type":"enum","value":null},{"deviceList":null,"multiple":false,"name":"cond","type":"enum","value":null},{"deviceList":null,"multiple":false,"name":"insertAct","type":"enum","value":""},{"deviceList":null,"multiple":false,"name":"ranMsg.3","type":"bool","value":""},{"deviceList":null,"multiple":false,"name":"useST","type":"bool","value":""},{"deviceList":null,"multiple":false,"name":"ranMsg.1","type":"bool","value":""},{"deviceList":null,"multiple":false,"name":"stopOnST","type":"bool","value":""},{"deviceList":null,"multiple":false,"name":"tCustomAttr5","type":"enum","value":"precipitationType"},{"deviceList":null,"multiple":false,"name":"actSubType.1","type":"enum","value":"getMsg"},{"deviceList":null,"multiple":false,"name":"actSubType.2","type":"enum","value":"getWaitEvents"},{"deviceList":null,"multiple":false,"name":"actSubType.3","type":"enum","value":"getMsg"},{"deviceList":null,"multiple":false,"name":"actSubType.4","type":"enum","value":"getLogMsg"},{"deviceList":null,"multiple":false,"name":"doneST","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"tstate5","type":"enum","value":"mix"},{"deviceList":null,"multiple":false,"name":"SSecs-1","type":"number","value":"0"},{"deviceList":null,"multiple":false,"name":"refreshActions","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"dValues","type":"bool","value":"true"},{"deviceList":null,"multiple":false,"name":"doneWaits","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"hasAll","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"anotherWait","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"uVar.1","type":"bool","value":""},{"deviceList":null,"multiple":true,"name":"mediaDevice.3","type":"capability.musicPlayer","value":null},{"deviceList":null,"multiple":false,"name":"cancelCapab","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"editToken","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"actType.1","type":"enum","value":"messageActs"},{"deviceList":null,"multiple":true,"name":"mediaDevice.1","type":"capability.musicPlayer","value":null},{"deviceList":null,"multiple":false,"name":"uVar.3","type":"bool","value":""},{"deviceList":null,"multiple":false,"name":"tCustomAttr-1","type":"enum","value":"precipitationType"},{"deviceList":null,"multiple":false,"name":"stopRule","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"stays-1","type":"bool","value":"false"},{"deviceList":{"134":"S\u00e9bastien\u2019s iPhone"},"multiple":true,"name":"note.3","type":"capability.notification","value":null},{"deviceList":null,"multiple":false,"name":"msg.1","type":"textarea","value":"It has started hailing and raining"},{"deviceList":{"134":"S\u00e9bastien\u2019s iPhone"},"multiple":true,"name":"note.1","type":"capability.notification","value":null},{"deviceList":null,"multiple":false,"name":"msg.3","type":"textarea","value":"It has stopped hailing and raining"},{"deviceList":null,"multiple":false,"name":"origLabel","type":"text","value":"Notification - Tempest detected Rain and Hail \ud83c\udf27"},{"deviceList":null,"multiple":false,"name":"eraseRule","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"useCond.5","type":"bool","value":""},{"deviceList":null,"multiple":false,"name":"actType.3","type":"enum","value":"messageActs"},{"deviceList":null,"multiple":true,"name":"logging","type":"enum","value":"[\"Events\",\"Triggers\",\"Actions\"]"},{"deviceList":null,"multiple":false,"name":"actType.2","type":"enum","value":"delayActs"},{"deviceList":{"4380":"Echo - Solarium Echo Show 5 on Hub"},"multiple":true,"name":"speakDevice.3","type":"capability.speechSynthesis","value":null},{"deviceList":null,"multiple":false,"name":"editCond","type":"enum","value":""},{"deviceList":null,"multiple":false,"name":"delayAct.1","type":"enum","value":"none"},{"deviceList":null,"multiple":false,"name":"delayAct.2","type":"enum","value":"none"},{"deviceList":null,"multiple":false,"name":"actType.4","type":"enum","value":"messageActs"},{"deviceList":null,"multiple":false,"name":"importAct","type":"enum","value":""},{"deviceList":null,"multiple":false,"name":"delayAct.3","type":"enum","value":"none"},{"deviceList":null,"multiple":false,"name":"delayAct.4","type":"enum","value":"none"},{"deviceList":null,"multiple":false,"name":"actType.6","type":"enum","value":""},{"deviceList":null,"multiple":false,"name":"updateRule","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"hasRule","type":"button","value":""},{"deviceList":{"4380":"Echo - Solarium Echo Show 5 on Hub"},"multiple":true,"name":"speakDevice.1","type":"capability.speechSynthesis","value":null},{"deviceList":null,"multiple":false,"name":"runAction","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"rCustomAttr_7","type":"enum","value":"precipitationType"},{"deviceList":null,"multiple":false,"name":"delete","type":"enum","value":""},{"deviceList":{"7423":"WeatherFlow Lite on Hub"},"multiple":false,"name":"rDev_7","type":"capability.","value":null},{"deviceList":{"7423":"WeatherFlow Lite on Hub"},"multiple":false,"name":"tDev5","type":"capability.*","value":null},{"deviceList":null,"multiple":false,"name":"actionCancel","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"ReltDev-1","type":"enum","value":""},{"deviceList":null,"multiple":false,"name":"editAct","type":"enum","value":""},{"deviceList":null,"multiple":false,"name":"pointless","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"state_7","type":"enum","value":"mix"},{"deviceList":null,"multiple":false,"name":"comments","type":"textarea","value":""},{"deviceList":null,"multiple":false,"name":"tCapab-1","type":"enum","value":"Custom Attribute"},{"deviceList":null,"multiple":true,"name":"cutAct","type":"enum","value":null},{"deviceList":null,"multiple":false,"name":"editST","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"speakVolume.1","type":"number","value":""},{"deviceList":null,"multiple":false,"name":"SHours-1","type":"number","value":"0"},{"deviceList":null,"multiple":false,"name":"cancelST","type":"button","value":""},{"deviceList":null,"multiple":false,"name":"speakVolume.3","type":"number","value":""},{"deviceList":null,"multiple":false,"name":"tstate-1","type":"enum","value":"none"},{"deviceList":null,"multiple":false,"name":"stays5","type":"bool","value":""},{"deviceList":null,"multiple":false,"name":"not7","type":"bool","value":"true"},{"deviceList":null,"multiple":false,"name":"hubitatQueryString","type":"text","value":null},{"deviceList":null,"multiple":false,"name":"oper","type":"enum","value":null},{"deviceList":null,"multiple":false,"name":"rCapab_7","type":"enum","value":"Custom Attribute"},{"deviceList":null,"multiple":false,"name":"SMins-1","type":"number","value":"2"},{"deviceList":null,"multiple":true,"name":"deleteAct","type":"enum","value":null}],"subscriptions":[{"handler":"allHandlerX","name":"precipitationType.mix","type":"DEVICE","typeId":7423,"typeName":"WeatherFlow Lite on Hub","filter":"true"}]}}}

/**
 *  WeatherFlow Lite driver for Hubitat
 *
 *  Copyright (c) 2019 Justin Walker
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 *
 *  v1.0.0 - initial version (2020-07-06)
 *  v1.0.1 - added strikeDistance (2020-07-08)
 *  v1.0.2 - initialize strike attrs (2020-07-09)
 *  v1.0.3 - added health check (2020-07-13)
 *  v1.0.4 - added support for publish rate, reducing resolution, and precipication rate (2020-08-01)
 *  v1.0.5 - bug fixes (2020-08-02)
 *  v1.0.6 - added null handling and battery status (2020-08-23)
 *
 */

import groovy.transform.Field
import java.text.SimpleDateFormat

metadata {
    definition (name: 'WeatherFlow Lite', namespace: 'augoisms', author: 'Justin Walker', importUrl: 'https://raw.githubusercontent.com/augoisms/hubitat/master/weatherflow/weatherflow.driver.groovy') {
        capability 'IlluminanceMeasurement'
        capability 'Initialize'
        capability 'PressureMeasurement'
        capability 'RelativeHumidityMeasurement'
        capability 'Sensor'
        capability 'TemperatureMeasurement'
        capability 'UltravioletIndex'
		command 'makeRain'
		command 'makeMix'

        attribute 'feelsLike', 'string'
        attribute 'heatIndex', 'number'
        attribute 'pressureTrend', 'enum', ['steady', 'falling', 'rising']
        attribute 'precipitationRate', 'number'
        attribute 'precipitationToday', 'number'
        attribute 'precipitationType', 'enum', ['none', 'rain', 'hail', 'mix']
        attribute 'solarRadiation', 'number'
        attribute 'strikeDetected', 'number'
        attribute 'strikeDistance', 'number'
        attribute 'windChill', 'number'
        attribute 'windDirection', 'string'
        attribute 'windSpeed', 'number'
    }

    preferences {
        input name: 'apiKey', type: 'string', title: '<b>WeatherFlow API Key</b>', description: '<div><i>Visit: <a href="https://weatherflow.github.io/SmartWeather/api/" target="_blank">WeatherFlow Smart Weather API</a></i></div><br>', required: true
        input name: 'stationId', type: 'string', title: '<b>Station ID</b>', description: '<div><i>ID of your station/hub.</i></div><br>', required: true
        
        if (connectionValidated()) {
            getStationDevices().each { k,v ->
                input name: "device_" + k, type: 'bool', title: "<b>Device: ${v}</b>", description: "<div><i>Subscribe to ${v} (${k}).</i></div><br>", required: true
            }

            input name: 'publishRate', type: 'enum', title: '<b>Publish Rate</b>', description: '<div><i>Rate that observations are published. Rain and strike events are always real time.</i></div><br>', required: true, options: [0:'Real Time', 180000:'3 Minutes', 300000:'5 Minutes', 600000:'10 Minutes', 900000:'15 Minutes', 1800000:'30 Minutes', 3600000:'1 Hour'], defaultValue: 0
            input name: 'reduceResolution', type: 'bool', title: '<b>Reduce Resolution</b>', description: '<div><i>Reduce resolution of Illuminance, Temperature, UV Index, and Wind Speed.</i></div><br>', required: true, defaultValue: true

            input name: 'unitTemp', type: 'enum', title: '<b>Unit - Temp</b>', required: true, options: ['F': 'Fahrenheit (F)', 'C': 'Celsius (C)'], defaultValue: 1
            input name: 'unitWindDirection', type: 'enum', title: '<b>Unit - Wind Direction</b>', required: true, options: ['cardinal': 'Cardinal', 'degrees': 'Degrees'], defaultValue: 1
            input name: 'unitWindSpeed', type: 'enum', title: '<b>Unit - Wind Speed</b>', required: true, options: ['mph': 'Miles (mph)', 'kph': 'Kilometers (kph)', 'kn': 'Knots', 'm/s': 'Meters (m/s)'], defaultValue: 'mph'
            input name: 'unitPressure', type: 'enum', title: '<b>Unit - Pressure</b>', required: true, options: ['mb': 'Millibars (mb)', 'inHg': 'Inches of mercury (inHg)'], defaultValue: 'inHg'
            input name: 'unitRain', type: 'enum', title: '<b>Unit - Rain</b>', required: true, options: ['in': 'Inches (in)', 'mm': 'Millimeters (mm)'], defaultValue: 'in'
            input name: 'unitDistance', type: 'enum', title: '<b>Unit - Distance</b>', required: true, options: ['mi': 'Miles (mi)', 'km': 'Kilometers (km)'], defaultValue: 'mi'
        }

        input name: 'logEnable', type: 'bool', title: 'Enable debug logging', defaultValue: false
    }
}

///
/// lifecycle events
///

void makeRain() {
	sendEvent(name: "precipitationType", value: "rain")
}

void makeMix() {
	sendEvent(name: "precipitationType", value: "mix")
}

void installed() { }

void updated() {
    log.info 'updated()'

    removeDataValue('devices')
    removeDataValue('device_agl')
    removeDataValue('station_elevation')
    state.clear()

    //Unschedule any existing schedules
    unschedule()

    // perform health check every 5 minutes
    runEvery5Minutes('healthCheck')   

    // disable logs in 30 minutes
    if (settings.logEnable) runIn(1800, logsOff) 

    initialize()
}

void initialize() {
    log.info 'initialize()'

    unschedule(initialize)

    // close websocket
    interfaces.webSocket.close()
    pauseExecution(1000)

    if (!connectionValidated()) {
        //log.warn 'Connection not validated. Cannot initialized.'
        return
    }

    // generate a new ws id
    String ws_id = UUID.randomUUID().toString()
    updateDataValue('ws_id', ws_id)
    
    try {
        // connect webSocket to weatherflow
        interfaces.webSocket.connect("wss://ws.weatherflow.com/swd/data?api_key=${apiKey}")
    } 
    catch (e) {
        log.error "webSocket.connect failed: ${e.message}"
    }
}

def parse(String description) {
    logDebug "parsed: $description"
    
    try {
        def response = null;
        response = new groovy.json.JsonSlurper().parseText(description)
        if (response == null){
            log.warn 'String description not parsed'
            return
        }

        switch (response.type) {
            case 'obs_air':
            case 'obs_sky':
            case 'obs_st':
                parseObservation(response)
                break
            case 'evt_precip':
                sendEvent(name: 'precipitationType', value: 'rain')
                logDebug 'precipitationType: rain'
                break
            case 'evt_strike':
                parseStrikeEvent(response)
                break
            case 'ack':
            case 'connection_opened':
                logDebug "response type: ${response.type}"
                break
            default:
                log.warn "Unhandled event: ${response}"
                break
        }
    }  
    catch(e) {
        log.error "Failed to parse json e = ${e}"
        log.debug description 
        return
    }
}

///
/// station metadata
///

Boolean connectionValidated() {   
    // api key and stationId are required
    if(!apiKey || !stationId) {
        log.warn 'apiKey and stationId are required'
        return false
    }

    // check for cached results
    if (getDataValue('devices')) return true 

    Boolean validated = false

    try {
        Map params = [
            uri: "https://swd.weatherflow.com/swd/rest/stations/${settings.stationId}?api_key=${settings.apiKey}",
            requestContentType: 'application/json',
            contentType: 'application/json'
        ]

        httpGet(params) { response -> 
            if (response.status != 200) {
                log.warn "Could not get station metadata. Error: ${response.status}"
            }
            else {                
                List devices = []
                response.data.stations[0].devices.each { it ->
                    // a device wihout a serial_number indicates that device is no longer active
                    // device_type of 'HB' is the hub
                    if (it.serial_number && it.device_type != 'HB') {
                        Map device = [
                            'device_id': it.device_id,
                            'device_type': it.device_type,
                            'serial_number': it.serial_number,
                            'agl': it.device_meta.agl,
                            'name': it.device_meta.name
                        ]
                        devices.add(device)
                    }

                    // save agl for Tempest (ST) or Air (AR)
                    if (it.device_type == 'ST' || it.device_type == 'AR') {
                        updateDataValue('device_agl', "${it.device_meta.agl}")
                    }
                }

                updateDataValue('devices', new groovy.json.JsonBuilder(devices).toString())

                // save station elevation
                updateDataValue('station_elevation', "${response.data.stations[0].station_meta.elevation}")
                
                validated = true
            }
        }
    }
    catch (e) {
        log.error e.message
    }

    return validated
}

Map getStationDevices() {
    String deviceData = getDataValue('devices')
    if (!deviceData) return [:]

    ArrayList devices = new groovy.json.JsonSlurper().parseText(deviceData)
    Map deviceConfig = [:]

    devices.each { it -> 
        deviceConfig << ["${it.device_id}":it.name]
    }

    return deviceConfig
}

///
/// websocket connection
///

void webSocketStatus(String status){
    logDebug "webSocketStatus: ${status}"

    if (status.startsWith('failure: ')) {
        log.warn "failure message from web socket ${status}"
        state.connection = 'disconnected'
        reconnectWebSocket()
    } 
    else if (status == 'status: open') {        
        log.info 'webSocket is open'
        state.connection = 'connected'

        requestData()

        // success! reset reconnect delay
        pauseExecution(1000)
        state.reconnectDelay = 1
    } 
    else if (status == 'status: closing'){
        log.warn 'webSocket connection closing.'
        state.connection = 'closing'
    } 
    else {
        log.warn "webSocket error: ${status}"
        state.connection = 'disconnected'
        reconnectWebSocket()
    }
}

void reconnectWebSocket() {
    // first delay is 2 seconds, doubles every time
    state.reconnectDelay = (state.reconnectDelay ?: 1) * 2

    // don't let delay get too crazy, max it out at 10 minutes
    if(state.reconnectDelay > 600) state.reconnectDelay = 600

    // if the station is offline, give it some time before trying to reconnect
    log.info "Reconnecting WebSocket in ${state.reconnectDelay} seconds."
    runIn(state.reconnectDelay, initialize, [overwrite: false])
}

void requestData() {
    getStationDevices().each { k, v ->
        if (settings["device_${k}"] == true) {
            Map listenStart = [
                'type': 'listen_start', // response types: ack, obs_air, obs_sky, obs_st, evt_strike, evt_precip
                'device_id': k,
                id: getDataValue('ws_id')
            ]
            String message = new groovy.json.JsonBuilder(listenStart).toString()
            interfaces.webSocket.sendMessage(message)
            log.info "Sent list_start for ${k}"
        }
    }
}

void healthCheck() {
    if (state.lastObservation != null) {
        // check if there have been any observations in the last 3 minutes
        if(state.lastObservation >= now() - (3 * 60 * 1000)) {
            // healthy
            logDebug 'healthCheck: healthy'
        }
        else {
            // not healthy
            log.warn 'healthCheck: not healthy'
            reconnectWebSocket()
        }
    }
    else {
        log.info 'No previous activity. Cannot determine health.'
    }
}

///
/// data parsing
///

@Field static Map EVT_STRIKE = [
    0: 'epoch',    // seconds utc,
    1: 'distance', // km
    2: 'energy'
]

@Field static Map OBS_AIR = [
    0:  'epoch',                             // seconds utc
    1:  'station_pressure',                  // mb
    2:  'air_temperature',                   // celcius
    3:  'relative_humidity',                 // %
    4:  'lightning_strike_count',
    5:  'lightning_strike_average_distance', // km
    6:  'battery',                           // volts
    7:  'report_interval'                    // minutes
]

@Field static Map OBS_SKY = [
    0:  'epoch',                             // seconds utc
    1:  'illuminance',                       // lux
    2:  'uv_index',
    3:  'rain_accumulation',                 // mm
    4:  'wind_lull',                         // m/s
    5:  'wind_avg',                          // m/s
    6:  'wind_gust',                         // m/s
    7:  'wind_direction',                    // degrees
    8:  'battery',                           // volts
    9:  'report_interval',                   // minutes
    10: 'solar_radiation',                   // W/m^2
    11: 'local_day_rain_accumulation',       // mm
    12: 'precipitation_type',                // 0 = none, 1 = rain, 2 = hail, 3 = rain/hail
    13: 'wind_sample_interval',              // seconds
    14: 'rain_accumulation_final',           // mm (rain check)
    15: 'local_day_rain_accumulation_final', // mm (rain check)
    16: 'precipitation_analysis_type'        // 0 = none, 1 = rain check with user display on, 2 = rain check with user display off
]

@Field static Map OBS_ST = [
    0:  'epoch',                             // seconds UTC
    1:  'wind_lull',                         // m/s
    2:  'wind_avg',                          // m/s
    3:  'wind_gust',                         // m/s
    4:  'wind_direction',                    // degrees
    5:  'wind_sample_interval',              // seconds
    6:  'station_pressure',                  // mb
    7:  'air_temperature',                   // celcius
    8:  'relative_humidity',                 // %
    9:  'illuminance',                       // lux
    10: 'uv_index',
    11: 'solar_radiation',                   // W/m^2
    12: 'rain_accumulation',                 // mm (for current interval)
    13: 'precipitation_type',                // 0 = none, 1 = rain, 2 = hail, 3 = rain/hail
    14: 'lightning_strike_average_distance', // km
    15: 'lightning_strike_count',
    16: 'battery',                           // volts
    17: 'report_interval',                   // minutes
    18: 'local_day_rain_accumulation',       // mm (for current day, midnight to midnight)
    19: 'rain_accumulation_final',           // mm (rain check)
    20: 'local_day_rain_accumulation_final', // mm (rain check)
    21: 'precipitation_analysis_type'        // 0 = none, 1 = rain check with user display on, 2 = rain check with user display off
]

void parseStrikeEvent(Map response) {
    Map strikeDistance = formatDistance(response.evt[1])
    sendEvent(name: 'strikeDetected', value: response.evt[0], descriptionText: formatDateTime((Long)response.evt[0]))
    sendEvent(name: 'strikeDistance', value: strikeDistance.value, unit: strikeDistance.unit)
    logDebug "strikeDetected: ${strikeDistance.value} ${strikeDistance.unit}"
}

void parseObservation(Map response) {

    Boolean publishAll = true
    Long rate = settings.publishRate as Long
    if (state.lastPublish != null && (now() - (Long)state.lastPublish) < rate) { publishAll = false }
    logDebug "observation received. publishing: ${publishAll}"

    Map obsMap
    switch (response.type) {
        case 'obs_air':
            obsMap = OBS_AIR
            break;
        case 'obs_sky':
            obsMap = OBS_SKY
            break;
        case 'obs_st':
            obsMap = OBS_ST
            break
    }

    response.obs[0].eachWithIndex { it, i -> 
        String field = obsMap[i]

        // do not process null values
        if (it == null) {
            // rain_accumulation_final and local_day_rain_accumulation_final are frequently null, so don't warn
            if (field != 'rain_accumulation_final' && field != 'local_day_rain_accumulation_final') {
                log.warn "null values for ${field}"
            }            
            return
        }

        if (publishAll && field == 'air_temperature') {
            Map temp = formatTemp(it)
            sendEvent(name: 'temperature', value: temp.value, unit: temp.unit)
            logDebug "${field}: ${temp.value} ${temp.unit}"
        }
        
        if (publishAll && field == 'battery') {
            String battery = formatBattery(it, response.type)
            state["battery_${response.device_id}"] = battery
            logDebug "${field}: ${battery}"
        }

        if (publishAll && field == 'illuminance') {
            Map illuminance = formatIlluminance(it)
            sendEvent(name: 'illuminance', value: illuminance.value, unit: illuminance.unit)
            logDebug "${field}: ${illuminance.value} ${illuminance.unit}"
        }
        
        if (publishAll && field == 'local_day_rain_accumulation') {
            Map precipAmount = formatPrecipitationAmount(it)
            sendEvent(name: 'precipitationToday', value: precipAmount.value, unit: precipAmount.unit)
            logDebug "${field}: ${precipAmount.value} ${precipAmount.unit}"
        }

        // evt_precip does not include the type, so always publish precipitation_type
        if (field == 'precipitation_type') {
            Map precipType = formatPrecipitationType(it)
            sendEvent(name: 'precipitationType', value: precipType.value)
            logDebug "${field}: ${precipType.value}"
        }

        if (field == 'rain_accumulation') {
            BigDecimal precipRate = parsePrecipitationRate(it as BigDecimal)
            if (publishAll) {
                Map precipAmount = formatPrecipitationAmount(precipRate)
                sendEvent(name: 'precipitationRate', value: precipAmount.value, unit: "${precipAmount.unit}/hr")
                logDebug "${field}: ${precipAmount.value} ${precipAmount.unit}/hr"
            }
        }

        if (publishAll && field == 'relative_humidity') {
            sendEvent(name: "humidity", value: it, unit: "%")
            logDebug "${field}: ${it}%"
        }

        if (publishAll && field == 'solar_radiation') {
            sendEvent(name: 'solarRadiation', value: it, unit: 'W/m^2')
            logDebug "${field}: ${it} W/m^2"
        }

        if (publishAll && field == 'station_pressure') {
            Map pressure = formatPressure(it)
            sendEvent(name: 'pressure', value: pressure.value, unit: pressure.unit)
            logDebug "${field}: ${pressure.value} ${pressure.unit}"
        }

        if (publishAll && field == 'uv_index') {
            def uvIndex = settings.reduceResolution ? Math.round(it) : it
            sendEvent(name: 'ultravioletIndex', value: uvIndex,)
            logDebug "${field}: ${uvIndex}"
        }

        if (publishAll && field == 'wind_avg') {
            Map windSpeed = formatWindSpeed(it)
            sendEvent(name: 'windSpeed', value: windSpeed.value, unit: windSpeed.unit)
            logDebug "${field}: ${windSpeed.value} ${windSpeed.unit}"
        }

        if (publishAll && field == 'wind_direction') {
            Map windDirection = formatWindDirection(it)
            sendEvent(name: 'windDirection', value: windDirection.value, unit: windDirection.unit)
            logDebug "${field}: ${windDirection.value} ${windDirection.unit}"
        }        
    }

    if (publishAll && response.containsKey('summary')) parseSummary(response)

    // init strike attributes so that they are available
    if (device.currentValue('strikeDetected') == null) { sendEvent(name: 'strikeDetected', value: 0) }
    if (device.currentValue('strikeDistance') == null) { sendEvent(name: 'strikeDistance', value: 0) }

    state.lastObservation = now()
    if (publishAll) state.lastPublish = now()
}

@Field static Map rainAccumulation = [:]

BigDecimal parsePrecipitationRate(BigDecimal rate)  {
    Long now = now()
    rainAccumulation[now] = rate

    def lastHour = rainAccumulation.findAll { (now - (it.key as Long)) < 3600000 }
    rainAccumulation = lastHour

    def total = lastHour.inject(0) { sum, k, v ->
        sum + v
    }

    logDebug "rainAccumulation: ${rainAccumulation}"

    return total
}

// summary is undocumented and therefore not guaranteed, however WeatherFlow recognizes people are using these values
// https://community.weatherflow.com/t/heads-up-forthcoming-changes-to-api-rest-ws/3601
// if these were to go away, they could be calculated in the driver
// https://weatherflow.github.io/SmartWeather/api/derived-metric-formulas.htm
void parseSummary(Map response) {
    if (response.summary.containsKey('pressure_trend') && response.summary.pressure_trend != null) {
        sendEvent(name: 'pressureTrend', value: response.summary.pressure_trend)
    }    
    
    if (response.summary.containsKey('feels_like') && response.summary.feels_like != null) {
        Map feelsLike = formatTemp(response.summary.feels_like)
        sendEvent(name: 'feelsLike', value: feelsLike.value, unit: feelsLike.unit)
    }
    
    if (response.summary.containsKey('heat_index') && response.summary.heat_index != null) {
        Map heatIndex = formatTemp(response.summary.heat_index)
        sendEvent(name: 'heatIndex', value: heatIndex.value, unit: heatIndex.unit)
    }
    
    if (response.summary.containsKey('wind_chill') && response.summary.wind_chill != null) {
        Map windChill = formatTemp(response.summary.wind_chill)
        sendEvent(name: 'windChill', value: windChill.value, unit: windChill.unit)
    }    
}

///
/// formatters
///

String formatBattery(BigDecimal voltage, String responseType) {
    if (responseType != 'obs_st') return "${voltage}V"

    String status
    if (voltage >= 2.455) { status = 'good' }
    else { mode = 'low'}

    return "${voltage}V ${status}"
}

String formatDateTime(Long dt) {
    Date t0 = new Date(dt * 1000)
    SimpleDateFormat tf = new SimpleDateFormat("E MMM dd HH:mm:ss z yyyy")
    tf.setTimeZone(location.timeZone)
    return tf.format(t0)
}

Map formatDistance(Integer value) {
    Boolean metric = (settings.unitDistance == 'km')
    return [
        value: metric ? round1(value) : round1(value / 1.609344),
        unit: metric ? 'km' : 'mi'
    ]
}

Map formatIlluminance(Integer value) {
    Integer illuminance = value

    if (settings.reduceResolution) {
        // round values to the nearest thousand
        if (value >= 1000) {
            illuminance = value % 1000 >= 500 ? value + 1000 - value % 1000 : value - (value % 1000)
        }
        // round values to the nearest hundred
        else if (value >= 100) {
            illuminance = value % 100 >= 50 ? value + 100 - value % 100 : value - (value % 100)
        }
        // round values to the nearest ten
        else if (value >= 10) {
            illuminance = value % 10 >= 5 ? value + 10 - value % 10 : value - (value % 10)
        }
    }

    return [
        value: illuminance,
        unit: 'lux'
    ]
}

Map formatPrecipitationAmount(BigDecimal value) {
    Boolean inches = (settings.unitRain == 'in')
    return [
        value: inches ? round2(value / 25.4) : round2(value),
        unit: inches ? 'in' : 'mm'
    ]
}

Map formatPrecipitationType(Integer value) {
    String type = 'none'
    switch (value) {
        case 0:
            type = 'none'
            break
        case 1:
            type = 'rain'
            break
        case 2:
            type = 'hail'
            break
        case 3:
            type = 'mix'
            break
    }

    return [
        value: type,
        unit: ''
    ]
}

Map formatPressure(BigDecimal value) {
    Boolean mb = (settings.unitPressure == 'mb')
    BigDecimal seaLevelPressure = calculateSeaLevelPressure(value)
    return [
        value: mb ? round1(seaLevelPressure) : round2(seaLevelPressure / 33.864),
        unit: mb ? 'mb' : 'inHg'
    ]
}

Map formatTemp(BigDecimal value) {
    Boolean fahrenheit = (settings.unitTemp == 'F')
    def temp = fahrenheit ? (value * 1.8) + 32 : value
    return [
        value: settings.reduceResolution ? round1Even(temp) : temp,
        unit: fahrenheit ? 'F' : 'C'
    ]
}

@Field static String[] CARDINAL_POINTS = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']

Map formatWindDirection(Integer value) {
    Boolean cardinal = (settings.unitWindDirection == 'cardinal')
    String windDirection = "${value}"

    if (cardinal) {
        Double val = Math.floor((value / 22.5) + 0.5);
        windDirection = CARDINAL_POINTS[(val % 16).intValue()]
    }

    return [
        value: windDirection,
        unit: cardinal ? '' : '°'
    ]
}

Map formatWindSpeed(BigDecimal value) {
    String unit = settings.unitWindSpeed
    BigDecimal speed = value

    switch (unit) {
        case 'mph':
            speed = value * 2.23694
            break
        case 'kph':
            speed = value * 3.6
            break
        case 'kn':
            speed = value * 1.9438445
            break
    }

    return [
        value: settings.reduceResolution ? Math.round(speed) : round2(speed),
        unit: unit
    ]
}

///
/// helper methods
///

// https://weatherflow.github.io/SmartWeather/api/derived-metric-formulas.html#sea-level-pressure
@Field static BigDecimal STANDARD_SEA_LEVEL_PRESSURE = 1013.25   // mb
@Field static BigDecimal GAS_CONSTANT_DRY_AIR = 287.05           // J/(kg*K)
@Field static BigDecimal STANDARD_ATMOSPHERE_LAPSE_RATE = 0.0065 // K/m
@Field static BigDecimal GRAVITY = 9.80665                       // m/s^2
@Field static BigDecimal STANDARD_SEA_LEVEL_TEMPERATURE = 288.15 // K

BigDecimal calculateSeaLevelPressure(BigDecimal input) {
    BigDecimal stationPressure = input
    BigDecimal elevation = new BigDecimal(getDataValue('station_elevation')) + new BigDecimal(getDataValue('device_agl'))

    BigDecimal calc_a_exp = (GAS_CONSTANT_DRY_AIR * STANDARD_ATMOSPHERE_LAPSE_RATE) / GRAVITY
    BigDecimal calc_a = Math.pow((STANDARD_SEA_LEVEL_PRESSURE / stationPressure).doubleValue(), calc_a_exp.doubleValue())

    BigDecimal calc_b = (elevation * STANDARD_ATMOSPHERE_LAPSE_RATE) / STANDARD_SEA_LEVEL_TEMPERATURE

    BigDecimal calc_c_exp = GRAVITY / (GAS_CONSTANT_DRY_AIR * STANDARD_ATMOSPHERE_LAPSE_RATE)
    BigDecimal calc_c = Math.pow((1 + (calc_a * calc_b)).doubleValue(), calc_c_exp.doubleValue())

    BigDecimal seaLevelPressure = stationPressure * calc_c

    return seaLevelPressure
}

void logDebug(str) {
    if (settings.logEnable) {
        log.debug str
    }
}

void logsOff() {
    log.info 'Logging disabled.'
    device.updateSetting('logEnable',[value:'false',type:'bool'])
}

BigDecimal round3(BigDecimal value) {
    return Math.round(value * 1000) / 1000
}

BigDecimal round2(BigDecimal value) {
    return Math.round(value * 100) / 100
}

BigDecimal round1(BigDecimal value) {
    return Math.round(value * 10) / 10
}

BigDecimal round1Even(BigDecimal value) {
    return Math.round(value / 0.2) * 0.2
}
1 Like

What I just posted above has two commands, makeRain and makeMix. They won't do anything to your setup other than give you those two commands. So just install that code in place of what you have now. Easy to revert.

I'll be back in an hour or so....

1 Like

That’s cool! I learned a little bit about Groovy coding today. :smiley:

So I triggered the “rain” event in the driver and this time it didn’t trigger the “rain and hail” rule - so working as expected.

I triggered the “mix” event in the driver and it did trigger the “rain and hail” rule - so working as expected.

Could it be that it was being triggered by the “rain” event because the “mix” event had never triggered before?

@bravenel: I assume this only impacts Rule. 5.x rules but would be needed for anything with a custom attribute trigger?

Thanks!

No. The only thing I can think of is that it had the wrong subscription. As I described above. These used to not include the ".mix" part at the end. But the code changed so that without that, it would trigger on any event for precipitationType.

1 Like

For some Custom Attribute triggers -- those for things like this, enumerations of words. Custom Attribute triggers for things with numeric values are still done the way they were before.

1 Like

Thanks! I’ll know to check the subscriptions if this ever happens again.